ChBysk commited on
Commit
e94f914
·
verified ·
1 Parent(s): 82e88b6

Upload folder using huggingface_hub

Browse files
Dockerfile CHANGED
@@ -15,6 +15,11 @@ WORKDIR /app
15
  ENV NODE_ENV=production \
16
  PORT=7860 \
17
  HOST=0.0.0.0
 
 
 
 
 
18
  COPY --from=build /app/.output ./.output
19
  EXPOSE 7860
20
  CMD ["node", ".output/server/index.mjs"]
 
15
  ENV NODE_ENV=production \
16
  PORT=7860 \
17
  HOST=0.0.0.0
18
+
19
+ # Install Python 3 and huggingface_hub library
20
+ RUN apt-get update && apt-get install -y python3 python3-pip && rm -rf /var/lib/apt/lists/*
21
+ RUN pip3 install --break-system-packages huggingface_hub
22
+
23
  COPY --from=build /app/.output ./.output
24
  EXPOSE 7860
25
  CMD ["node", ".output/server/index.mjs"]
src/routes/api/upload-to-dataset.ts ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { zipSync, strToU8 } from "fflate";
3
+ import { z } from "zod";
4
+ import { writeFileSync, unlinkSync } from "fs";
5
+ import { execSync } from "child_process";
6
+
7
+ const PostSchema = z.object({
8
+ id: z.string(),
9
+ fileUrl: z.string().url(),
10
+ ext: z.string().default("jpg"),
11
+ previewUrl: z.string().optional(),
12
+ sampleUrl: z.string().optional(),
13
+ tags: z.array(z.string()).default([]),
14
+ source: z.string().optional(),
15
+ rating: z.string().optional(),
16
+ score: z.number().optional(),
17
+ fileSize: z.number().optional(),
18
+ width: z.number().optional(),
19
+ height: z.number().optional(),
20
+ md5: z.string().optional(),
21
+ uploader: z.union([z.string(), z.number()]).optional(),
22
+ createdAt: z.string().optional(),
23
+ rawJson: z.string().optional(),
24
+ });
25
+
26
+ const BodySchema = z.object({
27
+ siteName: z.string().default("grabber"),
28
+ posts: z.array(PostSchema).min(1).max(200),
29
+ excludeTags: z.array(z.string()).default([]),
30
+ hfToken: z.string().min(1, "HF token is required"),
31
+ datasetName: z.string().min(1, "Dataset name is required"),
32
+ });
33
+
34
+ function sanitize(s: string) {
35
+ return s.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 60);
36
+ }
37
+
38
+ function humanSize(n?: number): string {
39
+ if (n == null || !Number.isFinite(n)) return "";
40
+ if (n < 1024) return `${n} B`;
41
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
42
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(2)} MB`;
43
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
44
+ }
45
+
46
+ function buildMarkdown(post: z.infer<typeof PostSchema>, excludeSet: Set<string>): string {
47
+ const dims = post.width && post.height ? `${post.width} x ${post.height}` : "";
48
+ const rows: [string, string][] = [
49
+ ["Source", post.source ?? ""],
50
+ ["Rating", post.rating ?? ""],
51
+ ["Score", post.score != null ? String(post.score) : ""],
52
+ ["Dimensions", dims],
53
+ ["File size", humanSize(post.fileSize)],
54
+ ["MD5", post.md5 ?? ""],
55
+ ["Uploader", post.uploader != null ? String(post.uploader) : ""],
56
+ ["Created at", post.createdAt ?? ""],
57
+ ["Original file", post.fileUrl],
58
+ ["Sample", post.sampleUrl ?? ""],
59
+ ["Preview", post.previewUrl ?? ""],
60
+ ["Ext", post.ext],
61
+ ];
62
+
63
+ const meta = rows.map(([k, v]) => `- **${k}:** ${v}`).join("\n");
64
+ const filteredTags = post.tags.filter((t) => !excludeSet.has(t.toLowerCase()));
65
+ const tags = filteredTags.length ? filteredTags.join(" ") : "";
66
+ let raw = "{}";
67
+ if (post.rawJson) {
68
+ try {
69
+ raw = JSON.stringify(JSON.parse(post.rawJson), null, 2);
70
+ } catch {
71
+ raw = post.rawJson;
72
+ }
73
+ }
74
+
75
+ return `# Post ${post.id}
76
+
77
+ ${meta}
78
+
79
+ ## Tags
80
+
81
+ ${tags}
82
+
83
+ ## Raw metadata
84
+
85
+ \`\`\`json
86
+ ${raw}
87
+ \`\`\`
88
+ `;
89
+ }
90
+
91
+ export const Route = createFileRoute("/api/upload-to-dataset")({
92
+ server: {
93
+ handlers: {
94
+ POST: async ({ request }) => {
95
+ let body: z.infer<typeof BodySchema>;
96
+ try {
97
+ body = BodySchema.parse(await request.json());
98
+ } catch (err) {
99
+ return new Response(
100
+ JSON.stringify({ error: err instanceof Error ? err.message : "Invalid body" }),
101
+ { status: 400, headers: { "Content-Type": "application/json" } },
102
+ );
103
+ }
104
+
105
+ const files: Record<string, Uint8Array> = {};
106
+ const errors: string[] = [];
107
+ const siteName = sanitize(body.siteName);
108
+ const excludeSet = new Set((body.excludeTags || []).map((t) => t.toLowerCase()));
109
+
110
+ // Generate files
111
+ await Promise.all(
112
+ body.posts.map(async (post) => {
113
+ const idSafe = sanitize(post.id);
114
+ const extSafe = sanitize(post.ext || "jpg");
115
+ const base = `${siteName}_${idSafe}`;
116
+
117
+ const filteredTags = post.tags.filter((t) => !excludeSet.has(t.toLowerCase()));
118
+
119
+ files[`${base}.txt`] = strToU8(filteredTags.join(" "));
120
+ files[`${idSafe}.md`] = strToU8(buildMarkdown(post, excludeSet));
121
+
122
+ try {
123
+ const res = await fetch(post.fileUrl, {
124
+ headers: { "User-Agent": "LovableGrabber/1.0" },
125
+ });
126
+ if (!res.ok) {
127
+ errors.push(`${post.id}: HTTP ${res.status}`);
128
+ return;
129
+ }
130
+ const buf = new Uint8Array(await res.arrayBuffer());
131
+ files[`${base}.${extSafe}`] = buf;
132
+ } catch (err) {
133
+ errors.push(`${post.id}: ${err instanceof Error ? err.message : "fetch failed"}`);
134
+ }
135
+ }),
136
+ );
137
+
138
+ if (errors.length) {
139
+ files["_errors.txt"] = strToU8(errors.join("\n"));
140
+ }
141
+
142
+ if (Object.keys(files).length === 0) {
143
+ return new Response(JSON.stringify({ error: "No files successfully compiled" }), {
144
+ status: 502,
145
+ headers: { "Content-Type": "application/json" },
146
+ });
147
+ }
148
+
149
+ // Build ZIP Sync
150
+ const zipped = zipSync(files, { level: 0 });
151
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
152
+ const filename = `grabber-${ts}.zip`;
153
+
154
+ // Temporary paths
155
+ const zipPath = `/tmp/${filename}`;
156
+ const pyScriptPath = `/tmp/hf_dataset_upload_${ts}.py`;
157
+
158
+ try {
159
+ // Write compiled zip to local temp folder
160
+ writeFileSync(zipPath, zipped);
161
+
162
+ // Write python uploader helper script
163
+ const pyCode = `
164
+ import sys
165
+ import os
166
+ from huggingface_hub import HfApi
167
+
168
+ token = sys.argv[1]
169
+ repo_id = sys.argv[2]
170
+ file_path = sys.argv[3]
171
+ path_in_repo = sys.argv[4]
172
+
173
+ api = HfApi()
174
+ try:
175
+ # Attempt to create the dataset repository if not exists
176
+ api.create_repo(repo_id=repo_id, repo_type="dataset", exist_ok=True, token=token)
177
+
178
+ # Upload the compiled zip to the dataset repository
179
+ api.upload_file(
180
+ path_or_fileobj=file_path,
181
+ path_in_repo=path_in_repo,
182
+ repo_id=repo_id,
183
+ repo_type="dataset",
184
+ token=token
185
+ )
186
+ print("SUCCESS")
187
+ sys.exit(0)
188
+ except Exception as e:
189
+ print(f"ERROR: {str(e)}", file=sys.stderr)
190
+ sys.exit(1)
191
+ `;
192
+ writeFileSync(pyScriptPath, pyCode);
193
+
194
+ // Execute python helper to create/upload the dataset
195
+ execSync(
196
+ `python3 "${pyScriptPath}" "${body.hfToken}" "${body.datasetName}" "${zipPath}" "${filename}"`,
197
+ );
198
+
199
+ return new Response(JSON.stringify({ success: true, filename }), {
200
+ status: 200,
201
+ headers: { "Content-Type": "application/json" },
202
+ });
203
+ } catch (execErr: unknown) {
204
+ let errMsg = "Execution error";
205
+ if (execErr instanceof Error) {
206
+ const stderr = (execErr as { stderr?: Buffer }).stderr;
207
+ errMsg = stderr ? stderr.toString() : execErr.message;
208
+ }
209
+ return new Response(
210
+ JSON.stringify({ error: `Hugging Face dataset upload failed: ${errMsg}` }),
211
+ {
212
+ status: 500,
213
+ headers: { "Content-Type": "application/json" },
214
+ },
215
+ );
216
+ } finally {
217
+ // Clean up temporary files
218
+ try {
219
+ unlinkSync(zipPath);
220
+ } catch {
221
+ // ignore
222
+ }
223
+ try {
224
+ unlinkSync(pyScriptPath);
225
+ } catch {
226
+ // ignore
227
+ }
228
+ }
229
+ },
230
+ },
231
+ },
232
+ });
src/routes/index.tsx CHANGED
@@ -48,6 +48,10 @@ function Grabber() {
48
  const [lightbox, setLightbox] = useState<NormalizedPost | null>(null);
49
  const [downloading, setDownloading] = useState(false);
50
  const [excludeTags, setExcludeTags] = useState("");
 
 
 
 
51
 
52
  const site = useMemo(() => sites.find((s) => s.id === siteId) ?? sites[0], [sites, siteId]);
53
 
@@ -116,6 +120,47 @@ function Grabber() {
116
  }
117
  };
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  return (
120
  <div className="min-h-screen bg-background text-foreground">
121
  <header className="border-b sticky top-0 bg-background/95 backdrop-blur z-10">
@@ -213,6 +258,47 @@ function Grabber() {
213
  Download ZIP
214
  </Button>
215
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  </header>
217
 
218
  <main className="max-w-7xl mx-auto p-4">
 
48
  const [lightbox, setLightbox] = useState<NormalizedPost | null>(null);
49
  const [downloading, setDownloading] = useState(false);
50
  const [excludeTags, setExcludeTags] = useState("");
51
+ const [hfToken, setHfToken] = useState("");
52
+ const [datasetName, setDatasetName] = useState("");
53
+ const [uploading, setUploading] = useState(false);
54
+ const [uploadStatus, setUploadStatus] = useState("");
55
 
56
  const site = useMemo(() => sites.find((s) => s.id === siteId) ?? sites[0], [sites, siteId]);
57
 
 
120
  }
121
  };
122
 
123
+ const uploadToDataset = async () => {
124
+ const chosen = posts.filter((p) => selected.has(p.id));
125
+ if (!chosen.length) {
126
+ alert("Please select some images to upload.");
127
+ return;
128
+ }
129
+ if (!hfToken.trim() || !datasetName.trim()) {
130
+ alert("Hugging Face write token and dataset name are required.");
131
+ return;
132
+ }
133
+ setUploading(true);
134
+ setUploadStatus("Uploading to Hugging Face...");
135
+ try {
136
+ const res = await fetch("/api/upload-to-dataset", {
137
+ method: "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify({
140
+ siteName: site.id,
141
+ posts: chosen,
142
+ excludeTags: excludeTags
143
+ .split(/\s+|,/)
144
+ .map((t) => t.trim().toLowerCase())
145
+ .filter(Boolean),
146
+ hfToken: hfToken.trim(),
147
+ datasetName: datasetName.trim(),
148
+ }),
149
+ });
150
+ const data = await res.json();
151
+ if (!res.ok) {
152
+ setUploadStatus(`Upload failed: ${data.error || "Unknown error"}`);
153
+ } else {
154
+ setUploadStatus(`Success! Uploaded ZIP as ${data.filename}`);
155
+ }
156
+ } catch (err: unknown) {
157
+ const errMsg = err instanceof Error ? err.message : "Network error";
158
+ setUploadStatus(`Upload failed: ${errMsg}`);
159
+ } finally {
160
+ setUploading(false);
161
+ }
162
+ };
163
+
164
  return (
165
  <div className="min-h-screen bg-background text-foreground">
166
  <header className="border-b sticky top-0 bg-background/95 backdrop-blur z-10">
 
258
  Download ZIP
259
  </Button>
260
  </div>
261
+
262
+ {/* Hugging Face Dataset Integration Panel */}
263
+ <div className="max-w-7xl mx-auto px-4 pb-3 flex gap-2 flex-wrap items-center border-t pt-3 mt-1 bg-muted/20">
264
+ <span className="font-semibold text-xs text-muted-foreground mr-1 uppercase tracking-wider">
265
+ Hugging Face Upload:
266
+ </span>
267
+ <Input
268
+ type="password"
269
+ placeholder="HF Write Token"
270
+ value={hfToken}
271
+ onChange={(e) => setHfToken(e.target.value)}
272
+ className="w-48 max-w-xs"
273
+ />
274
+ <Input
275
+ placeholder="dataset-username/dataset-name"
276
+ value={datasetName}
277
+ onChange={(e) => setDatasetName(e.target.value)}
278
+ className="w-64 max-w-xs"
279
+ />
280
+ <Button
281
+ onClick={uploadToDataset}
282
+ disabled={!selected.size || uploading || !hfToken.trim() || !datasetName.trim()}
283
+ variant="secondary"
284
+ >
285
+ {uploading ? (
286
+ <Loader2 className="w-4 h-4 mr-1 animate-spin" />
287
+ ) : (
288
+ <Plus className="w-4 h-4 mr-1" />
289
+ )}
290
+ Upload to HF Dataset
291
+ </Button>
292
+ {uploadStatus && (
293
+ <span
294
+ className={`text-xs font-semibold ${
295
+ uploadStatus.includes("failed") ? "text-destructive" : "text-emerald-600"
296
+ }`}
297
+ >
298
+ {uploadStatus}
299
+ </span>
300
+ )}
301
+ </div>
302
  </header>
303
 
304
  <main className="max-w-7xl mx-auto p-4">