reelforge 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.
package/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # reelforge
2
+
3
+ > CLI for [ReelForge Studio](https://github.com/puke3615/ReelForge) — every REST API exposed as a command, with `--help` available at every level.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g reelforge
9
+ ```
10
+
11
+ Or use directly without install:
12
+
13
+ ```bash
14
+ npx reelforge <command>
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ `reelforge` ships pointing at the hosted instance (`https://reelforge.timor419.com`). Get an activation code from the admin, log in once, and call:
20
+
21
+ ```bash
22
+ npm install -g reelforge
23
+ reelforge login JKxxxxxxxxxxxxxx # bind your API key
24
+ reelforge whoami # balance + api_keys
25
+ reelforge create "为什么我们还没找到外星文明?" -o space.mp4
26
+ ```
27
+
28
+ That's the whole story — no server to run.
29
+
30
+ ### Self-hosting
31
+
32
+ If you want to run your own ReelForge Studio (own LLM / RunningHub keys, your own pricing) clone the upstream repo, `pnpm dev`, then point the CLI at it:
33
+
34
+ ```bash
35
+ reelforge --server http://localhost:8501 health
36
+ # or persist:
37
+ export REELFORGE_SERVER=http://localhost:8501
38
+ # or via `reelforge login <key> --server http://localhost:8501`
39
+ ```
40
+
41
+ ## Global options
42
+
43
+ | flag | description |
44
+ |---|---|
45
+ | `-s, --server <url>` | ReelForge server URL (overrides `$REELFORGE_SERVER`; default `https://reelforge.timor419.com`) |
46
+ | `-k, --api-key <key>` | API key (overrides `$REELFORGE_API_KEY` and `reelforge login` saved key) |
47
+ | `--json` | Output raw JSON instead of pretty text — pipe-friendly |
48
+ | `--quiet` | Suppress informational messages on stderr |
49
+ | `-v, --version` | Show CLI version |
50
+ | `-h, --help` | Show help (works on every sub-command) |
51
+
52
+ ## Command map
53
+
54
+ Run `reelforge <command> --help` for full details on any of these.
55
+
56
+ ### Core capabilities
57
+
58
+ | command | what it does |
59
+ |---|---|
60
+ | `llm chat -p <text>` | Send one prompt to the configured LLM |
61
+ | `llm presets` | List built-in provider presets |
62
+ | `tts edge -t <text> -o out.mp3` | Local Edge TTS synthesis |
63
+ | `tts workflow -t <text> -w <workflow>` | ComfyUI TTS workflow (clone-style, IndexTTS etc.) |
64
+ | `tts voices [--locale zh]` | List supported Edge TTS voices |
65
+ | `images generate -p <prompt> -w <workflow>` | Image generation via ComfyUI / RunningHub |
66
+ | `images analyze -i <image>` | Reverse-describe an image |
67
+ | `videos generate -p <prompt> -w <workflow>` | Atomic video generation |
68
+ | `videos analyze -i <video>` | Reverse-describe a video |
69
+
70
+ ### Content generation
71
+
72
+ | command | what it does |
73
+ |---|---|
74
+ | `content narration -t <topic>` | Generate N narration sentences from a topic |
75
+ | `content split -s <script>` | Split a fixed script into narrations |
76
+ | `content image-prompts -i <file>` | English image prompts from narration list |
77
+ | `content title -c <content>` | Generate a short video title |
78
+ | `content asset-script --intent ... --assets <file>` | Asset-based scene script |
79
+
80
+ ### Composition
81
+
82
+ | command | what it does |
83
+ |---|---|
84
+ | `templates list [--size 1080x1920] [--type image]` | List HTML frame templates |
85
+ | `templates preview <key> [-o out.png]` | Render a template preview |
86
+ | `frames render -t <key> --title ... --text ...` | Render a single composed frame to PNG |
87
+ | `compositions concat <v1> <v2> -o out.mp4` | FFmpeg concat (+ optional BGM) |
88
+ | `compositions bgm -i video.mp4 --bgm bgm.mp3 -o out.mp4` | Add background music |
89
+ | `compositions image-to-video -i img.png -a aud.mp3 -o out.mp4` | Build video from image + audio |
90
+ | `compositions overlay -v video.mp4 --overlay overlay.png -o out.mp4` | Overlay PNG on video |
91
+
92
+ ### End-to-end pipelines
93
+
94
+ All `pipelines *` commands submit an **async task** and (by default) poll until it finishes with a live progress indicator on stderr. Use `--no-wait` to return immediately with a `task_id`, then `reelforge tasks wait <id>` later.
95
+
96
+ | command | what it does |
97
+ |---|---|
98
+ | `pipelines standard -t <topic\|script>` | Topic / script → narration → frames → final MP4 |
99
+ | `pipelines asset-based --intent ... --assets <file>` | User assets → AI-arranged scene video |
100
+ | `pipelines digital-human -w <wf> -i <portrait> -t <text>` | Digital human speaker |
101
+ | `pipelines i2v -w <wf> -i <image>` | Image-to-video |
102
+ | `pipelines action-transfer -w <wf> --reference-video <v> --character-image <img>` | Action transfer |
103
+
104
+ ### Resources
105
+
106
+ | command | what it does |
107
+ |---|---|
108
+ | `workflows list [--source runninghub] [--kind image]` | Browse ComfyUI workflows |
109
+ | `bgm list / upload <file> / delete <name>` | Manage background music |
110
+ | `files list / upload <file> / download <path> / delete <path>` | Manage user assets |
111
+
112
+ ### System
113
+
114
+ | command | what it does |
115
+ |---|---|
116
+ | `config get` | Read server config (keys masked) |
117
+ | `config set <key> <value>` | Update a dotted-path setting (e.g. `llm.api_key sk-xxx`) |
118
+ | `config patch <file>` | Apply a JSON-merge patch |
119
+ | `tasks list [--status running]` | List recent tasks |
120
+ | `tasks get <id>` / `tasks wait <id>` / `tasks cancel <id>` | Task lifecycle |
121
+ | `history list / get <id> / delete <id>` | Browse / delete completed runs |
122
+ | `health` | Server health + capability check |
123
+
124
+ ## Examples
125
+
126
+ ```bash
127
+ # 1. End-to-end "text → video" with zero external APIs (Edge TTS + static template)
128
+ reelforge pipelines standard \
129
+ -t "Hello world. This is scene one.\n\nThis is scene two." \
130
+ --mode fixed --title "Smoke Test" \
131
+ --frame-template 1080x1920/static_default.html \
132
+ --tts-voice en-US-AriaNeural -o smoke.mp4
133
+
134
+ # 2. Inspect existing tasks & redownload a finished video
135
+ reelforge tasks list --limit 5
136
+ reelforge history get <task-id> --download recovered.mp4
137
+
138
+ # 3. JSON pipe for automation
139
+ reelforge workflows list --kind image --json | jq '.workflows[].key'
140
+
141
+ # 4. Configure & test LLM
142
+ reelforge config set llm.api_key sk-xxxxx
143
+ reelforge config set llm.base_url https://dashscope.aliyuncs.com/compatible-mode/v1
144
+ reelforge config set llm.model qwen-plus
145
+ reelforge llm chat -p 'one-sentence summary of antifragile'
146
+ ```
147
+
148
+ ## Tip — getting unstuck
149
+
150
+ Every level has `--help`:
151
+
152
+ ```bash
153
+ reelforge --help # top-level overview
154
+ reelforge pipelines --help # list of pipelines
155
+ reelforge pipelines standard --help # full option reference
156
+ reelforge tts edge --help # one specific command
157
+ ```
158
+
159
+ ## Development & publishing
160
+
161
+ ```bash
162
+ cd cli
163
+ npm install
164
+ npm run build
165
+ npm link # makes `reelforge` available globally for testing
166
+ reelforge health # try it out
167
+
168
+ # Publish to npm
169
+ npm publish --access public
170
+ ```
171
+
172
+ ## License
173
+
174
+ Apache-2.0
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ // Thin shim so `reelforge` is invokable globally. The actual implementation
3
+ // is bundled to dist/ by `npm run build`.
4
+ import("../dist/index.js").catch((err) => {
5
+ // eslint-disable-next-line no-console
6
+ console.error(err?.stack || err);
7
+ process.exit(1);
8
+ });
package/dist/client.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * HTTP client for the ReelForge Studio REST API.
3
+ *
4
+ * Resolution:
5
+ * server = --server flag > $REELFORGE_SERVER > ~/.reelforge/config.json > https://reelforge.timor419.com
6
+ * api_key = setApiKey() (from --api-key flag) > $REELFORGE_API_KEY > ~/.reelforge/config.json > none
7
+ *
8
+ * Auth: if an api_key is resolved, requests automatically get
9
+ * `Authorization: Bearer <api_key>` (unless the caller pre-sets one).
10
+ */
11
+ import { readFileSync } from "node:fs";
12
+ import path from "node:path";
13
+ import os from "node:os";
14
+ const DEFAULT_SERVER = "https://reelforge.timor419.com";
15
+ function loadConfigSync() {
16
+ try {
17
+ const p = path.join(os.homedir(), ".reelforge", "config.json");
18
+ const raw = readFileSync(p, "utf-8");
19
+ const parsed = JSON.parse(raw);
20
+ return typeof parsed === "object" && parsed ? parsed : {};
21
+ }
22
+ catch {
23
+ return {};
24
+ }
25
+ }
26
+ const fileConfig = loadConfigSync();
27
+ let overrideServer = null;
28
+ let overrideApiKey = null;
29
+ export function setServer(url) {
30
+ overrideServer = url.replace(/\/$/, "");
31
+ }
32
+ export function getServer() {
33
+ if (overrideServer)
34
+ return overrideServer;
35
+ const env = process.env.REELFORGE_SERVER;
36
+ if (env)
37
+ return env.replace(/\/$/, "");
38
+ if (fileConfig.server)
39
+ return fileConfig.server.replace(/\/$/, "");
40
+ return DEFAULT_SERVER;
41
+ }
42
+ export function setApiKey(key) {
43
+ overrideApiKey = key;
44
+ }
45
+ export function getApiKey() {
46
+ if (overrideApiKey)
47
+ return overrideApiKey;
48
+ if (process.env.REELFORGE_API_KEY)
49
+ return process.env.REELFORGE_API_KEY;
50
+ if (fileConfig.api_key)
51
+ return fileConfig.api_key;
52
+ return null;
53
+ }
54
+ export class ApiCallError extends Error {
55
+ status;
56
+ code;
57
+ details;
58
+ constructor(err) {
59
+ super(err.message);
60
+ this.status = err.status;
61
+ this.code = err.code;
62
+ this.details = err.details;
63
+ }
64
+ }
65
+ async function request(path, init = {}) {
66
+ const url = `${getServer()}${path.startsWith("/") ? path : `/${path}`}`;
67
+ const headers = new Headers(init.headers);
68
+ const key = getApiKey();
69
+ if (key && !headers.has("Authorization") && !headers.has("authorization")) {
70
+ headers.set("Authorization", `Bearer ${key}`);
71
+ }
72
+ let res;
73
+ try {
74
+ res = await fetch(url, { ...init, headers });
75
+ }
76
+ catch (err) {
77
+ const msg = err instanceof Error ? err.message : String(err);
78
+ throw new ApiCallError({
79
+ status: 0,
80
+ message: `Network error contacting ${url}: ${msg}`,
81
+ });
82
+ }
83
+ const text = await res.text();
84
+ let json = null;
85
+ try {
86
+ json = text ? JSON.parse(text) : null;
87
+ }
88
+ catch {
89
+ if (!res.ok) {
90
+ throw new ApiCallError({ status: res.status, message: text.slice(0, 500) });
91
+ }
92
+ return text;
93
+ }
94
+ if (!res.ok) {
95
+ const err = json?.error;
96
+ throw new ApiCallError({
97
+ status: res.status,
98
+ code: err?.code,
99
+ message: err?.message || `HTTP ${res.status}`,
100
+ details: err?.details,
101
+ });
102
+ }
103
+ return json;
104
+ }
105
+ export function get(path) {
106
+ return request(path);
107
+ }
108
+ export function post(path, body) {
109
+ return request(path, {
110
+ method: "POST",
111
+ headers: { "Content-Type": "application/json" },
112
+ body: JSON.stringify(body),
113
+ });
114
+ }
115
+ export function patch(path, body) {
116
+ return request(path, {
117
+ method: "PATCH",
118
+ headers: { "Content-Type": "application/json" },
119
+ body: JSON.stringify(body),
120
+ });
121
+ }
122
+ export function del(path) {
123
+ return request(path, { method: "DELETE" });
124
+ }
125
+ export async function uploadMultipart(path, fields) {
126
+ const form = new FormData();
127
+ for (const [k, v] of Object.entries(fields))
128
+ form.append(k, v);
129
+ return request(path, { method: "POST", body: form });
130
+ }
@@ -0,0 +1,69 @@
1
+ import { get, setApiKey, setServer, getServer } from "../client.js";
2
+ import { loadConfig, saveConfig, deleteConfig, getConfigPath } from "../utils/config-file.js";
3
+ import { info, success, print, table } from "../utils/output.js";
4
+ export function registerAuth(program) {
5
+ program
6
+ .command("login <api_key>")
7
+ .description("Save your API key (and optionally server URL) to ~/.reelforge/config.json")
8
+ .helpOption("-h, --help", "show help")
9
+ .option("--server <url>", "also persist a custom server URL")
10
+ .addHelpText("after", [
11
+ "",
12
+ "Examples:",
13
+ " reelforge login JK1234567890ABCDEF # bind key, use default server",
14
+ " reelforge login JK1234567890ABCDEF --server http://nas:8501 # bind key + custom server",
15
+ "",
16
+ "The key is verified against /api/v1/me before being saved.",
17
+ ].join("\n"))
18
+ .action(async (apiKey, opts) => {
19
+ if (opts.server)
20
+ setServer(opts.server);
21
+ setApiKey(apiKey);
22
+ // verify
23
+ const me = await get("/api/v1/me");
24
+ const existing = await loadConfig();
25
+ const saved = await saveConfig({
26
+ ...existing,
27
+ api_key: apiKey,
28
+ ...(opts.server ? { server: opts.server } : {}),
29
+ });
30
+ success(`Saved → ${saved}`);
31
+ info(`Account: ${me.account.account_number}${me.account.label ? ` · ${me.account.label}` : ""}`);
32
+ info(`Balance: ${me.account.balance} ${me.account.unit_name}`);
33
+ info(`Server: ${getServer()}`);
34
+ });
35
+ program
36
+ .command("logout")
37
+ .description("Delete the locally saved API key (and server)")
38
+ .helpOption("-h, --help", "show help")
39
+ .action(async () => {
40
+ const path = getConfigPath();
41
+ const deleted = await deleteConfig();
42
+ if (deleted) {
43
+ success(`Deleted ${path}`);
44
+ }
45
+ else {
46
+ info(`No config to delete (${path} not found)`);
47
+ }
48
+ });
49
+ program
50
+ .command("whoami")
51
+ .description("Show the currently logged-in account: balance, api_keys, usage stats")
52
+ .helpOption("-h, --help", "show help")
53
+ .action(async () => {
54
+ const me = await get("/api/v1/me");
55
+ print({
56
+ account: me.account,
57
+ });
58
+ if (me.api_keys.length) {
59
+ info(`API keys (${me.api_keys.length}):`);
60
+ table(me.api_keys.map((k) => ({
61
+ id: k.id,
62
+ api_key: k.api_key,
63
+ label: k.label ?? "",
64
+ status: k.status,
65
+ last_used_at: k.last_used_at ?? "—",
66
+ })));
67
+ }
68
+ });
69
+ }
@@ -0,0 +1,45 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { del, get, uploadMultipart } from "../client.js";
4
+ import { print, table, success, bytes } from "../utils/output.js";
5
+ export function registerBgm(program) {
6
+ const bgm = program
7
+ .command("bgm")
8
+ .description("Manage background-music files")
9
+ .helpOption("-h, --help", "show help");
10
+ bgm
11
+ .command("list")
12
+ .description("List all BGM files (default + user-uploaded)")
13
+ .helpOption("-h, --help", "show help")
14
+ .action(async () => {
15
+ const r = await get("/api/v1/bgm");
16
+ table(r.bgm.map((b) => ({
17
+ name: b.name,
18
+ size: bytes(b.size_bytes),
19
+ source: b.source,
20
+ path: b.relative_path,
21
+ })));
22
+ });
23
+ bgm
24
+ .command("upload <file>")
25
+ .description("Upload a new BGM file (saved under data/bgm/)")
26
+ .helpOption("-h, --help", "show help")
27
+ .action(async (file) => {
28
+ const abs = path.resolve(file);
29
+ const buf = fs.readFileSync(abs);
30
+ const name = path.basename(abs);
31
+ const fileObj = new File([new Uint8Array(buf)], name, { type: "audio/mpeg" });
32
+ const r = await uploadMultipart("/api/v1/bgm", { file: fileObj });
33
+ success(`Uploaded ${name}`);
34
+ print(r);
35
+ });
36
+ bgm
37
+ .command("delete <name>")
38
+ .alias("rm")
39
+ .description("Delete a user-uploaded BGM (default repo BGM is protected)")
40
+ .helpOption("-h, --help", "show help")
41
+ .action(async (name) => {
42
+ const r = await del(`/api/v1/bgm/${encodeURIComponent(name)}`);
43
+ print(r);
44
+ });
45
+ }
@@ -0,0 +1,95 @@
1
+ import path from "node:path";
2
+ import { post } from "../client.js";
3
+ import { downloadTo } from "../utils/download.js";
4
+ import { print, success } from "../utils/output.js";
5
+ export function registerCompositions(program) {
6
+ const comp = program
7
+ .command("compositions")
8
+ .alias("compose")
9
+ .description("FFmpeg compositions: concat / add BGM / image+audio→video / overlay")
10
+ .helpOption("-h, --help", "show help");
11
+ comp
12
+ .command("concat <videos...>")
13
+ .description("Concatenate multiple MP4 files into one")
14
+ .helpOption("-h, --help", "show help")
15
+ .option("-o, --output <file>", "output path (REQUIRED)")
16
+ .option("--method <method>", "demuxer | filter", "demuxer")
17
+ .option("--bgm <path>", "optional BGM file to mix in")
18
+ .option("--bgm-volume <n>", "BGM volume (0..1)", parseFloat, 0.2)
19
+ .option("--bgm-mode <mode>", "loop | once", "loop")
20
+ .addHelpText("after", "\nExample:\n reelforge compositions concat a.mp4 b.mp4 c.mp4 -o final.mp4 --bgm bgm/default.mp3")
21
+ .action(async (videos, opts) => {
22
+ if (!opts.output)
23
+ throw new Error("--output is required");
24
+ const r = await post("/api/v1/compositions/concat", {
25
+ videos: videos.map((v) => path.resolve(v)),
26
+ method: opts.method,
27
+ bgm_path: opts.bgm,
28
+ bgm_volume: opts.bgmVolume,
29
+ bgm_mode: opts.bgmMode,
30
+ });
31
+ await downloadTo(r.url, opts.output);
32
+ success(`Saved → ${opts.output}`);
33
+ print(r);
34
+ });
35
+ comp
36
+ .command("bgm")
37
+ .description("Add background music to an existing video")
38
+ .helpOption("-h, --help", "show help")
39
+ .requiredOption("-i, --input <file>", "input video path")
40
+ .requiredOption("--bgm <file>", "BGM audio file")
41
+ .option("-o, --output <file>", "output path (REQUIRED)")
42
+ .option("--volume <n>", "BGM volume (0..1)", parseFloat, 0.2)
43
+ .option("--mode <mode>", "loop | once", "loop")
44
+ .action(async (opts) => {
45
+ if (!opts.output)
46
+ throw new Error("--output is required");
47
+ const r = await post("/api/v1/compositions/bgm", {
48
+ video: path.resolve(opts.input),
49
+ bgm: path.resolve(opts.bgm),
50
+ volume: opts.volume,
51
+ mode: opts.mode,
52
+ });
53
+ await downloadTo(r.url, opts.output);
54
+ success(`Saved → ${opts.output}`);
55
+ print(r);
56
+ });
57
+ comp
58
+ .command("image-to-video")
59
+ .description("Build a video from a single image + an audio track")
60
+ .helpOption("-h, --help", "show help")
61
+ .requiredOption("-i, --image <file>", "input image")
62
+ .requiredOption("-a, --audio <file>", "input audio")
63
+ .option("-o, --output <file>", "output path (REQUIRED)")
64
+ .option("--fps <n>", "frames per second", parseInt, 30)
65
+ .action(async (opts) => {
66
+ if (!opts.output)
67
+ throw new Error("--output is required");
68
+ const r = await post("/api/v1/compositions/image-to-video", {
69
+ image: path.resolve(opts.image),
70
+ audio: path.resolve(opts.audio),
71
+ fps: opts.fps,
72
+ });
73
+ await downloadTo(r.url, opts.output);
74
+ success(`Saved → ${opts.output}`);
75
+ print(r);
76
+ });
77
+ comp
78
+ .command("overlay")
79
+ .description("Overlay a transparent PNG on top of a video")
80
+ .helpOption("-h, --help", "show help")
81
+ .requiredOption("-v, --video <file>", "input video")
82
+ .requiredOption("--overlay <file>", "overlay PNG (with transparency)")
83
+ .option("-o, --output <file>", "output path (REQUIRED)")
84
+ .action(async (opts) => {
85
+ if (!opts.output)
86
+ throw new Error("--output is required");
87
+ const r = await post("/api/v1/compositions/overlay", {
88
+ video: path.resolve(opts.video),
89
+ overlay_image: path.resolve(opts.overlay),
90
+ });
91
+ await downloadTo(r.url, opts.output);
92
+ success(`Saved → ${opts.output}`);
93
+ print(r);
94
+ });
95
+ }
@@ -0,0 +1,65 @@
1
+ import fs from "node:fs/promises";
2
+ import { get, patch } from "../client.js";
3
+ import { print } from "../utils/output.js";
4
+ export function registerConfig(program) {
5
+ const cfg = program
6
+ .command("config")
7
+ .description("Read or update the server config.yaml (LLM / ComfyUI / RunningHub keys)")
8
+ .helpOption("-h, --help", "show help");
9
+ cfg
10
+ .command("get")
11
+ .description("Print the current config (API keys masked)")
12
+ .helpOption("-h, --help", "show help")
13
+ .action(async () => {
14
+ const r = await get("/api/v1/config");
15
+ print(r);
16
+ });
17
+ cfg
18
+ .command("set <key> <value>")
19
+ .description("Update a single config value using dotted path, e.g. `llm.api_key sk-xxx`")
20
+ .helpOption("-h, --help", "show help")
21
+ .addHelpText("after", [
22
+ "",
23
+ "Examples:",
24
+ " reelforge config set llm.api_key sk-xxxxxx",
25
+ " reelforge config set llm.base_url https://dashscope.aliyuncs.com/compatible-mode/v1",
26
+ " reelforge config set llm.model qwen-plus",
27
+ " reelforge config set comfyui.runninghub_api_key rh-xxxxxx",
28
+ " reelforge config set comfyui.runninghub_instance_type plus",
29
+ ].join("\n"))
30
+ .action(async (key, value) => {
31
+ const parts = key.split(".");
32
+ const patchBody = {};
33
+ let cur = patchBody;
34
+ for (let i = 0; i < parts.length - 1; i++) {
35
+ const next = {};
36
+ cur[parts[i]] = next;
37
+ cur = next;
38
+ }
39
+ cur[parts[parts.length - 1]] = coerce(value);
40
+ const r = await patch("/api/v1/config", patchBody);
41
+ print(r);
42
+ });
43
+ cfg
44
+ .command("patch <file>")
45
+ .description("Apply a JSON-merge patch file to the config")
46
+ .helpOption("-h, --help", "show help")
47
+ .action(async (file) => {
48
+ const body = JSON.parse(await fs.readFile(file, "utf-8"));
49
+ const r = await patch("/api/v1/config", body);
50
+ print(r);
51
+ });
52
+ }
53
+ function coerce(s) {
54
+ if (s === "true")
55
+ return true;
56
+ if (s === "false")
57
+ return false;
58
+ if (s === "null")
59
+ return null;
60
+ if (/^-?\d+$/.test(s))
61
+ return parseInt(s, 10);
62
+ if (/^-?\d+\.\d+$/.test(s))
63
+ return parseFloat(s);
64
+ return s;
65
+ }