agens-studio 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,279 @@
1
+ import { Router } from 'express';
2
+ import { optimizePrompt, optimizeNegativePrompt, rewritePrompt, generateCandidates } from '../services/mimoClient.js';
3
+ import { describeImage, descriptionToText, reviewImage } from '../services/mimoVision.js';
4
+ import * as mem from '../services/promptMemory.js';
5
+
6
+ const router = Router();
7
+
8
+ /**
9
+ * POST /api/prompt/optimize
10
+ * body: { text, mode, extraHints?, imageUrl? }
11
+ * - imageUrl: 图生图模式必填(本地上传图路径),用于识图
12
+ * 返回: { optimized, sampleId, dimensions?, imageDescription? }
13
+ * - dimensions: 文生图/图生图的结构化维度(前端分维度展示)
14
+ * - imageDescription: 图生图/图生视频的识图结果(前端展示原图识别)
15
+ * - imageDescriptions: 多图生视频/关键帧的多张识图结果
16
+ *
17
+ * 识图流程(两阶段):
18
+ * 1. mimo-v2.5 识图(单张逐个,带缓存)→ 结构化描述
19
+ * 2. mimo-v2.5-pro 基于识图结果 + 用户要求 → 优化
20
+ * 需要识图的模式:img2img(1张)、image2video(1张)、multi2video(N张)、keyframe(N张)
21
+ */
22
+ router.post('/optimize', async (req, res) => {
23
+ const { text, mode, extraHints, imageUrl, imageUrls } = req.body || {};
24
+ if (!text || !text.trim()) {
25
+ return res.status(400).json({ error: '请输入要优化的提示词' });
26
+ }
27
+ try {
28
+ // 判断该模式是否需要识图
29
+ const needVision = ['img2img', 'image2video', 'multi2video', 'keyframe'].includes(mode);
30
+ // 统一成数组处理:图生图/图生视频单图 → [imageUrl];多图/关键帧 → imageUrls
31
+ const visionUrls = needVision
32
+ ? (mode === 'img2img' || mode === 'image2video'
33
+ ? (imageUrl ? [imageUrl] : [])
34
+ : (Array.isArray(imageUrls) ? imageUrls : []))
35
+ : [];
36
+
37
+ if (needVision && visionUrls.length === 0) {
38
+ return res.status(400).json({ error: '该模式需要上传参考图才能优化' });
39
+ }
40
+ if (mode === 'keyframe' && visionUrls.length < 2) {
41
+ return res.status(400).json({ error: '关键帧动画至少需要首帧和尾帧' });
42
+ }
43
+
44
+ // 识图(单张逐个,带缓存)
45
+ let imageDescriptionObj = null; // 单图模式:识图对象
46
+ let imageDescription = ''; // 单图:拼成文本
47
+ let multiImageDescriptions = []; // 多图:[{ index, label, description, cached }]
48
+ let imageCached = false;
49
+
50
+ if (visionUrls.length === 1) {
51
+ const vision = await describeImage(visionUrls[0]);
52
+ imageDescriptionObj = vision.description;
53
+ imageDescription = descriptionToText(imageDescriptionObj);
54
+ imageCached = vision.cached;
55
+ } else if (visionUrls.length > 1) {
56
+ // 多图:逐个识,拼成「图1:...;图2:...」
57
+ const labels = mode === 'keyframe'
58
+ ? ['首帧', '尾帧', '中间帧1', '中间帧2', '中间帧3']
59
+ : ['图1', '图2', '图3', '图4', '图5', '图6'];
60
+ const parts = [];
61
+ for (let i = 0; i < visionUrls.length; i++) {
62
+ const vision = await describeImage(visionUrls[i]);
63
+ const label = labels[i] || `图${i + 1}`;
64
+ multiImageDescriptions.push({
65
+ index: i,
66
+ label,
67
+ description: vision.description,
68
+ cached: vision.cached,
69
+ });
70
+ parts.push(`${label}:${descriptionToText(vision.description)}`);
71
+ }
72
+ imageDescription = parts.join('\n');
73
+ imageCached = multiImageDescriptions.every((d) => d.cached);
74
+ }
75
+
76
+ // 优化(带上识图上下文)
77
+ const result = await optimizePrompt({
78
+ text: text.trim(),
79
+ mode,
80
+ extraHints,
81
+ imageDescription,
82
+ });
83
+
84
+ // 存入记忆库(带识图结果)
85
+ const sample = await mem.addSample({
86
+ kind: 'positive',
87
+ original: text.trim(),
88
+ optimized: result.optimized,
89
+ mode,
90
+ imageDescription,
91
+ });
92
+
93
+ res.json({
94
+ optimized: result.optimized,
95
+ dimensions: result.dimensions || null,
96
+ sampleId: sample.id,
97
+ imageDescription: imageDescriptionObj,
98
+ multiImageDescriptions,
99
+ imageCached,
100
+ });
101
+ } catch (e) {
102
+ res.status(500).json({ error: '优化失败:' + e.message });
103
+ }
104
+ });
105
+
106
+ /**
107
+ * POST /api/prompt/optimize-negative
108
+ * body: { text, mode, positive? }
109
+ * 返回: { optimized, sampleId }
110
+ *
111
+ * 反向提示词优化,独立记忆库(kind=negative)
112
+ */
113
+ router.post('/optimize-negative', async (req, res) => {
114
+ const { text, mode, positive } = req.body || {};
115
+ if (!text || !text.trim()) {
116
+ return res.status(400).json({ error: '请输入要优化的反向提示词' });
117
+ }
118
+ try {
119
+ const { optimized } = await optimizeNegativePrompt({
120
+ text: text.trim(),
121
+ mode,
122
+ positive: positive || '',
123
+ });
124
+ const sample = await mem.addSample({ kind: 'negative', original: text.trim(), optimized, mode });
125
+ res.json({ optimized, sampleId: sample.id });
126
+ } catch (e) {
127
+ res.status(500).json({ error: '反向优化失败:' + e.message });
128
+ }
129
+ });
130
+
131
+ /**
132
+ * POST /api/prompt/feedback
133
+ * body: { sampleId, rating, correction? } rating: 1=赞, -1=踩
134
+ * - correction: 点踩原因(层级1:让模型知道具体哪里不行)
135
+ */
136
+ router.post('/feedback', async (req, res) => {
137
+ const { sampleId, rating, correction } = req.body || {};
138
+ if (!sampleId) return res.status(400).json({ error: '缺少 sampleId' });
139
+ if (![1, -1].includes(rating)) return res.status(400).json({ error: 'rating 必须是 1 或 -1' });
140
+ const patch = { rating };
141
+ // 点踩时附带原因;点赞时清空原因(用户改主意了)
142
+ if (rating === -1 && typeof correction === 'string') patch.correction = correction;
143
+ if (rating === 1) patch.correction = '';
144
+ const s = await mem.updateSample(sampleId, patch);
145
+ if (!s) return res.status(404).json({ error: '样本不存在' });
146
+ res.json({ ok: true });
147
+ });
148
+
149
+ /**
150
+ * POST /api/prompt/rewrite
151
+ * 重写:用户带具体修改指令重新优化(层级2)
152
+ * body: { sampleId, instruction } sampleId 指明要重写哪条,instruction 是修改要求
153
+ * 返回: { optimized } 并把重写历史写回该样本
154
+ */
155
+ router.post('/rewrite', async (req, res) => {
156
+ const { sampleId, instruction } = req.body || {};
157
+ if (!sampleId) return res.status(400).json({ error: '缺少 sampleId' });
158
+ if (!instruction || !instruction.trim()) {
159
+ return res.status(400).json({ error: '请输入修改要求' });
160
+ }
161
+ // 取出该样本,拿到原始输入/模式/上次结果/识图描述
162
+ const sample = await mem.getSample(sampleId);
163
+ if (!sample) return res.status(404).json({ error: '样本不存在' });
164
+
165
+ try {
166
+ const result = await rewritePrompt({
167
+ text: sample.original,
168
+ mode: sample.mode,
169
+ lastResult: sample.optimized,
170
+ instruction: instruction.trim(),
171
+ isNegative: sample.kind === 'negative',
172
+ // 图生图重写时把识图结果带上,让 [保留] 维度有依据
173
+ imageDescription: sample.imageDescription || '',
174
+ });
175
+ // 写回重写历史(注意:updateSample 会把 optimized 更新为最新重写结果)
176
+ await mem.updateSample(sampleId, {
177
+ rewrite: { instruction: instruction.trim(), result: result.optimized, accepted: false },
178
+ });
179
+ // 文生图/图生图:返回 dimensions 让前端分维度渲染
180
+ res.json({
181
+ optimized: result.optimized,
182
+ dimensions: result.dimensions || null,
183
+ });
184
+ } catch (e) {
185
+ res.status(500).json({ error: '重写失败:' + e.message });
186
+ }
187
+ });
188
+
189
+ /**
190
+ * POST /api/prompt/adopt
191
+ * 标记某个样本被「采用」(用户点了采用按钮)
192
+ * body: { sampleId }
193
+ */
194
+ router.post('/adopt', async (req, res) => {
195
+ const { sampleId } = req.body || {};
196
+ if (!sampleId) return res.status(400).json({ error: '缺少 sampleId' });
197
+ const s = await mem.updateSample(sampleId, { adoptedDelta: 1 });
198
+ if (!s) return res.status(404).json({ error: '样本不存在' });
199
+ res.json({ ok: true });
200
+ });
201
+
202
+ /**
203
+ * POST /api/prompt/candidates
204
+ * 生成 3 个不同方向的候选提示词
205
+ * body: { text, mode, imageUrl?, imageUrls? }
206
+ * 返回: { candidates: [{ label, optimized, dimensions }] }
207
+ */
208
+ router.post('/candidates', async (req, res) => {
209
+ const { text, mode, imageUrl, imageUrls } = req.body || {};
210
+ if (!text || !text.trim()) {
211
+ return res.status(400).json({ error: '请输入提示词' });
212
+ }
213
+ try {
214
+ // 识图(需要识图的模式)
215
+ let imageDescription = '';
216
+ const needVision = ['img2img', 'image2video', 'multi2video', 'keyframe'].includes(mode);
217
+ if (needVision) {
218
+ const urls = (mode === 'img2img' || mode === 'image2video')
219
+ ? (Array.isArray(imageUrls) && imageUrls.length ? imageUrls : (imageUrl ? [imageUrl] : []))
220
+ : (Array.isArray(imageUrls) ? imageUrls : []);
221
+ if (urls.length === 0) {
222
+ return res.status(400).json({ error: '该模式需要上传参考图' });
223
+ }
224
+ const parts = [];
225
+ const labels = mode === 'keyframe' ? ['首帧', '尾帧'] : ['图1', '图2', '图3'];
226
+ for (let i = 0; i < urls.length; i++) {
227
+ const v = await describeImage(urls[i]);
228
+ parts.push(`${labels[i] || '图' + (i+1)}:${descriptionToText(v.description)}`);
229
+ }
230
+ imageDescription = parts.join('\n');
231
+ }
232
+
233
+ const { candidates } = await generateCandidates({
234
+ text: text.trim(),
235
+ mode,
236
+ imageDescription,
237
+ });
238
+ const optimized = candidates
239
+ .map((c) => c.optimized || '')
240
+ .filter(Boolean)
241
+ .join('\n\n');
242
+ const sample = await mem.addSample({
243
+ kind: 'positive',
244
+ original: text.trim(),
245
+ optimized,
246
+ mode,
247
+ imageDescription,
248
+ });
249
+ res.json({ candidates, sampleId: sample.id, imageDescription: imageDescription || null });
250
+ } catch (e) {
251
+ res.status(500).json({ error: '候选生成失败:' + e.message });
252
+ }
253
+ });
254
+
255
+ /**
256
+ * POST /api/prompt/review
257
+ * AI 复看:看 agnes 生成的图,评价 + 改进提示词
258
+ * body: { assetPath, prompt, mode }
259
+ * 返回: { rating, comment, problems, suggestion, improvedPrompt }
260
+ */
261
+ router.post('/review', async (req, res) => {
262
+ const { assetPath, prompt, mode } = req.body || {};
263
+ if (!assetPath) return res.status(400).json({ error: '缺少 assetPath' });
264
+ try {
265
+ const review = await reviewImage(assetPath, prompt || '', mode || 'image');
266
+ res.json(review);
267
+ } catch (e) {
268
+ res.status(500).json({ error: '复看失败:' + e.message });
269
+ }
270
+ });
271
+
272
+ /**
273
+ * GET /api/prompt/stats —— 记忆库统计(调试/管理用)
274
+ */
275
+ router.get('/stats', async (req, res) => {
276
+ res.json(await mem.stats());
277
+ });
278
+
279
+ export default router;
@@ -0,0 +1,19 @@
1
+ import { Router } from 'express';
2
+ import { getTask, recentTasks } from '../services/taskManager.js';
3
+
4
+ const router = Router();
5
+
6
+ /** GET /api/task/:id 查单个任务状态 */
7
+ router.get('/:id', (req, res) => {
8
+ const t = getTask(req.params.id);
9
+ if (!t) return res.status(404).json({ error: '任务不存在' });
10
+ res.json(t);
11
+ });
12
+
13
+ /** GET /api/task 最近任务列表(供前端任务队列展示) */
14
+ router.get('/', (req, res) => {
15
+ const limit = Math.min(parseInt(req.query.limit, 10) || 20, 100);
16
+ res.json({ items: recentTasks(limit) });
17
+ });
18
+
19
+ export default router;
@@ -0,0 +1,257 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { config } from '../../config.js';
4
+
5
+ function authHeaders(apiKey) {
6
+ return {
7
+ 'Content-Type': 'application/json',
8
+ Authorization: `Bearer ${apiKey}`,
9
+ };
10
+ }
11
+
12
+ function ratioToImageSize(ratio) {
13
+ const map = {
14
+ '1:1': '2048x2048',
15
+ '3:4': '1536x2048',
16
+ '4:3': '2048x1536',
17
+ '16:9': '2560x1440',
18
+ '9:16': '1440x2560',
19
+ };
20
+ return map[ratio] || '2048x2048';
21
+ }
22
+
23
+ function ratioToVideoSize(ratio, resolution) {
24
+ const r = resolution || '1080p';
25
+ const short = r === '2k' ? 1440 : r === '1080p' ? 1080 : 720;
26
+ const long = Math.round(short * 16 / 9);
27
+ const map = {
28
+ '16:9': { width: long, height: short },
29
+ '9:16': { width: short, height: long },
30
+ '1:1': { width: short, height: short },
31
+ '4:3': { width: Math.round(short * 4 / 3), height: short },
32
+ '3:4': { width: short, height: Math.round(short * 4 / 3) },
33
+ };
34
+ return map[ratio] || { width: 1920, height: 1080 };
35
+ }
36
+
37
+ function styleToPromptSuffix(style) {
38
+ const map = {
39
+ photographic: ', photographic, realistic, high detail',
40
+ anime: ', anime style, illustration',
41
+ '3d': ', 3D render, cgi',
42
+ art: ', artistic, digital art',
43
+ };
44
+ return map[style] || '';
45
+ }
46
+
47
+ function durationToFrames(seconds) {
48
+ const fps = 30;
49
+ const candidates = [81, 121, 161, 201, 241, 281, 321, 361, 401, 441];
50
+ const target = seconds * fps;
51
+ let best = candidates[0];
52
+ let bestDiff = Math.abs(candidates[0] - target);
53
+ for (const c of candidates) {
54
+ const d = Math.abs(c - target);
55
+ if (d < bestDiff) { bestDiff = d; best = c; }
56
+ }
57
+ return { num_frames: best, frame_rate: fps };
58
+ }
59
+
60
+ async function localImageToString(localPath, asDataUri) {
61
+ let absPath;
62
+ if (localPath.startsWith('/uploads/')) {
63
+ absPath = path.join(config.dirs.uploads, path.basename(localPath));
64
+ } else if (localPath.startsWith('/assets/images/')) {
65
+ absPath = path.join(config.dirs.images, path.basename(localPath));
66
+ } else {
67
+ return localPath;
68
+ }
69
+ const buf = await fs.readFile(absPath);
70
+ const b64 = buf.toString('base64');
71
+ if (!asDataUri) return b64;
72
+ const ext = path.extname(absPath).slice(1).toLowerCase();
73
+ const mime = 'image/' + (ext === 'jpg' ? 'jpeg' : (ext || 'png'));
74
+ return 'data:' + mime + ';base64,' + b64;
75
+ }
76
+
77
+ async function agnesFetch(url, opts, options) {
78
+ const opts2 = options || {};
79
+ const retries = opts2.retries != null ? opts2.retries : 3;
80
+ const timeoutMs = opts2.timeoutMs != null ? opts2.timeoutMs : 120000;
81
+ let lastErr;
82
+ for (let attempt = 0; attempt <= retries; attempt++) {
83
+ const controller = new AbortController();
84
+ const timer = setTimeout(function () { controller.abort(); }, timeoutMs);
85
+ try {
86
+ const res = await fetch(url, Object.assign({}, opts, { signal: controller.signal }));
87
+ clearTimeout(timer);
88
+ if ((res.status === 429 || res.status === 503) && attempt < retries) {
89
+ const t = await res.text().catch(function () { return ''; });
90
+ console.log('[agnes] ' + res.status + ' busy, retry ' + (attempt + 1) + ' in 5s: ' + t.slice(0, 100));
91
+ await new Promise(function (r) { setTimeout(r, 5000); });
92
+ continue;
93
+ }
94
+ return res;
95
+ } catch (e) {
96
+ clearTimeout(timer);
97
+ lastErr = e;
98
+ if (attempt < retries) {
99
+ console.log('[agnes] network error, retry ' + (attempt + 1) + ': ' + e.message);
100
+ await new Promise(function (r) { setTimeout(r, 3000); });
101
+ continue;
102
+ }
103
+ }
104
+ }
105
+ throw lastErr || new Error('agnes request failed after retries');
106
+ }
107
+
108
+ function agensFetch(url, opts, options) {
109
+ return agnesFetch(url, opts, options);
110
+ }
111
+
112
+ export function buildImageRequestBody({ prompt, params = {} }) {
113
+ const cfg = config.agens.image;
114
+ const size = ratioToImageSize(params.ratio);
115
+ const fullPrompt = prompt + styleToPromptSuffix(params.style);
116
+ const body = {
117
+ model: cfg.model,
118
+ prompt: fullPrompt,
119
+ size: size,
120
+ extra_body: { response_format: 'url' },
121
+ };
122
+ if (Array.isArray(params.imageUrls) && params.imageUrls.length > 0) {
123
+ body.image = params.imageUrls;
124
+ }
125
+ return body;
126
+ }
127
+
128
+ export async function generateImage(input) {
129
+ const params = input.params || {};
130
+ const cfg = config.agens.image;
131
+ const imageUrls = Array.isArray(params.imageUrls) ? params.imageUrls : [];
132
+ let requestParams = params;
133
+ if (imageUrls.length > 0) {
134
+ const images = [];
135
+ for (let i = 0; i < imageUrls.length; i++) {
136
+ images.push(await localImageToString(imageUrls[i], true));
137
+ }
138
+ requestParams = { ...params, imageUrls: images };
139
+ }
140
+ const body = buildImageRequestBody({ prompt: input.prompt, params: requestParams });
141
+ const res = await agnesFetch(cfg.baseUrl + cfg.endpoint, {
142
+ method: 'POST',
143
+ headers: authHeaders(cfg.apiKey),
144
+ body: JSON.stringify(body),
145
+ });
146
+ const text = await res.text();
147
+ if (!res.ok) {
148
+ let detail = text;
149
+ try { detail = JSON.stringify(JSON.parse(text)); } catch (e) {}
150
+ throw new Error('agnes image failed (' + res.status + '): ' + detail.slice(0, 500));
151
+ }
152
+ let data;
153
+ try { data = JSON.parse(text); } catch (e) { throw new Error('agnes image response invalid JSON'); }
154
+ var item = data.data && data.data[0];
155
+ var url = item && item.url;
156
+ if (!url) {
157
+ if (item && item.b64_json) {
158
+ return { url: 'data:image/png;base64,' + item.b64_json, mime: 'image/png', meta: data };
159
+ }
160
+ throw new Error('agnes image response missing url');
161
+ }
162
+ return { url: url, mime: 'image/png', meta: data };
163
+ }
164
+
165
+ export async function generateVideo(input) {
166
+ const mode = input.mode;
167
+ const prompt = input.prompt;
168
+ const imageUrls = input.imageUrls || [];
169
+ const params = input.params || {};
170
+ const cfg = config.agens.video;
171
+ const wh = ratioToVideoSize(params.ratio, params.resolution);
172
+ const fr = durationToFrames(parseInt(params.duration, 10) || 5);
173
+ var images = [];
174
+ if (imageUrls.length) {
175
+ for (let i = 0; i < imageUrls.length; i++) {
176
+ images.push(await localImageToString(imageUrls[i], false));
177
+ }
178
+ }
179
+ const body = {
180
+ model: cfg.model,
181
+ prompt: prompt,
182
+ width: wh.width,
183
+ height: wh.height,
184
+ num_frames: fr.num_frames,
185
+ frame_rate: fr.frame_rate,
186
+ };
187
+ if (params.negativePrompt && params.negativePrompt.trim()) {
188
+ body.negative_prompt = params.negativePrompt.trim();
189
+ }
190
+ if (mode === 'image2video') {
191
+ body.image = images[0];
192
+ } else if (mode === 'multi2video') {
193
+ body.extra_body = { image: images };
194
+ } else if (mode === 'keyframe') {
195
+ body.extra_body = { image: images, mode: 'keyframes' };
196
+ }
197
+ const res = await agnesFetch(cfg.baseUrl + cfg.createEndpoint, {
198
+ method: 'POST',
199
+ headers: authHeaders(cfg.apiKey),
200
+ body: JSON.stringify(body),
201
+ });
202
+ const text = await res.text();
203
+ if (!res.ok) {
204
+ let detail = text;
205
+ try { detail = JSON.stringify(JSON.parse(text)); } catch (e) {}
206
+ throw new Error('agnes video create failed (' + res.status + '): ' + detail.slice(0, 500));
207
+ }
208
+ let data;
209
+ try { data = JSON.parse(text); } catch (e) { throw new Error('agnes video response invalid JSON'); }
210
+ const videoId = data.video_id || data.id || data.task_id;
211
+ if (!videoId) throw new Error('agnes video response missing video_id');
212
+ return { videoId: videoId, raw: data };
213
+ }
214
+
215
+ export async function getTaskStatus(videoId) {
216
+ const cfg = config.agens.video;
217
+ const url = cfg.queryBaseUrl + cfg.queryPath + '?video_id=' + encodeURIComponent(videoId) + '&model_name=' + encodeURIComponent(cfg.model);
218
+ const res = await agnesFetch(url, { method: 'GET', headers: authHeaders(cfg.apiKey) });
219
+ const text = await res.text();
220
+ if (!res.ok) {
221
+ throw new Error('agnes video query failed (' + res.status + '): ' + text.slice(0, 200));
222
+ }
223
+ let data;
224
+ try { data = JSON.parse(text); } catch (e) { throw new Error('agnes video query invalid JSON'); }
225
+ const status = normalizeStatus(data.status);
226
+ const result = {};
227
+ if (typeof data.progress === 'number') result.progress = data.progress;
228
+ if (status === 'success') {
229
+ result.videoUrl = data.remixed_from_video_id
230
+ || data.video_url
231
+ || data.url
232
+ || data.output
233
+ || (data.result && data.result.video_url)
234
+ || (data.result && data.result.url)
235
+ || (Array.isArray(data.output) ? data.output[0] : null);
236
+ result.duration = data.seconds ? parseFloat(data.seconds) : undefined;
237
+ if (!result.videoUrl) {
238
+ console.error('[agnes] video success but no URL found, response:', JSON.stringify(data).slice(0, 500));
239
+ }
240
+ }
241
+ if (status === 'failed') {
242
+ result.error = (data.error && (data.error.message || data.error)) || data.message || 'video generation failed';
243
+ }
244
+ return { status: status, progress: result.progress, result: result, raw: data };
245
+ }
246
+
247
+ function normalizeStatus(s) {
248
+ const v = String(s || '').toLowerCase();
249
+ if (['completed', 'complete', 'succeed', 'success', 'done'].indexOf(v) >= 0) return 'success';
250
+ if (['fail', 'failed', 'error'].indexOf(v) >= 0) return 'failed';
251
+ if (['in_progress', 'processing', 'running', 'generating'].indexOf(v) >= 0) return 'processing';
252
+ return 'pending';
253
+ }
254
+
255
+ export function isMockMode() {
256
+ return false;
257
+ }