flash-story-app 1.0.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/LICENSE +7 -0
- package/README.md +94 -0
- package/dist/cli.js +2 -0
- package/dist/env.js +1 -0
- package/dist/index.js +1 -0
- package/dist/serve.js +5127 -0
- package/package.json +43 -0
- package/static/assets/index-3MnGe9um.js +5491 -0
- package/static/icon/128.png +0 -0
- package/static/icon/32.png +0 -0
- package/static/icon/48.png +0 -0
- package/static/index.html +13 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
FlashStory — proprietary software.
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This package is UNLICENSED: installing and running it (e.g. via `npx
|
|
6
|
+
flashstory`) is permitted; modification, copying and redistribution are NOT
|
|
7
|
+
granted. See the store listing / PRD for the product context.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# flash-story-app
|
|
2
|
+
|
|
3
|
+
One command runs the whole FlashStory shot editor — the B/S version (WEB.md):
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx flash-story-app # starts the server on http://127.0.0.1:8787 (web UI + API)
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It is also a CLI for automation:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx flash-story-app health # server status
|
|
13
|
+
npx flash-story-app scene list # projects
|
|
14
|
+
npx flash-story-app scene get --project <id> # read a scene
|
|
15
|
+
npx flash-story-app entity add --project <id> --id c1 --name 老张 --color red
|
|
16
|
+
npx flash-story-app transform set --project <id> --id c1 --json '[{…}]'
|
|
17
|
+
npx flash-story-app export submit --project <id> # + status / result
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Everything it does goes through the same reducer validation as the editor UI.
|
|
21
|
+
For the full command reference and the Claude Code Skill workflow, see
|
|
22
|
+
`skills/flashstory/SKILL.md` in the repo (or the FlashStory Skill catalog).
|
|
23
|
+
|
|
24
|
+
Environment:
|
|
25
|
+
|
|
26
|
+
| var | default | meaning |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `FLASHSTORY_DATA_DIR` | `~/.flashstory/data` | project + export + asset storage |
|
|
29
|
+
| `FLASHSTORY_PORT` | `8787` | HTTP port |
|
|
30
|
+
| `FLASHSTORY_HOST` | `127.0.0.1` | bind address (LAN requires explicit opt-in) |
|
|
31
|
+
| `FLASHSTORY_STATIC_DIR` | packaged `static/` | where the web UI is served from |
|
|
32
|
+
| `FLASHSTORY_ENV_FILE` | `./.env` | .env file path (see below; missing file = no-op) |
|
|
33
|
+
| `FLASHSTORY_OPENAI_API_KEY` | — | LLM gateway key (never reaches the browser) |
|
|
34
|
+
| `FLASHSTORY_OPENAI_BASE_URL` | `https://api.openai.com/v1` | OpenAI-compatible endpoint |
|
|
35
|
+
| `FLASHSTORY_OPENAI_MODEL` | `gpt-4o-mini` | default model |
|
|
36
|
+
| `FLASHSTORY_OPENAI_MODELS` | = MODEL | **fallback** selectable model list (comma/space separated), used only when the endpoint's live `/v1/models` probe fails |
|
|
37
|
+
| `FLASHSTORY_SERVER` | `http://127.0.0.1:8787` | CLI client target (flag `--server` wins) |
|
|
38
|
+
|
|
39
|
+
Serve-mode CLI flags (override env / .env): `--port <n>` `--host <h>`
|
|
40
|
+
`--data-dir <dir>` `--open` (open the editor in the default browser after boot).
|
|
41
|
+
|
|
42
|
+
### .env support
|
|
43
|
+
|
|
44
|
+
Both the server and the CLI client load a `.env` at startup. Lookup order:
|
|
45
|
+
|
|
46
|
+
1. `FLASHSTORY_ENV_FILE` — explicit path
|
|
47
|
+
2. `./.env` — the working directory (wherever you run it; a repo-root or
|
|
48
|
+
server `.env` is fine as long as you start from there)
|
|
49
|
+
3. `~/.flashstory/.env` — machine-global config, so `npx flash-story-app` from
|
|
50
|
+
any directory still picks up your key
|
|
51
|
+
|
|
52
|
+
Process environment wins over .env — a shell `export` beats the file. The
|
|
53
|
+
startup banner prints which file was used and whether the LLM is configured:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
env file: ~/.flashstory/.env
|
|
57
|
+
data dir: /Users/me/.flashstory/data
|
|
58
|
+
LLM: configured · endpoint: https://api.deepseek.com/v1 · default model: deepseek-chat
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
FLASHSTORY_PORT=8787
|
|
65
|
+
FLASHSTORY_OPENAI_API_KEY=sk-…
|
|
66
|
+
FLASHSTORY_OPENAI_BASE_URL=https://api.deepseek.com/v1
|
|
67
|
+
FLASHSTORY_OPENAI_MODEL=deepseek-chat
|
|
68
|
+
FLASHSTORY_OPENAI_MODELS=deepseek-chat, deepseek-reasoner, gpt-4o-mini
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The settings dialog reads the model list from the server: a live probe of the
|
|
72
|
+
endpoint's `/v1/models` (cached 60s with a 2.5s timeout), falling back to
|
|
73
|
+
`FLASHSTORY_OPENAI_MODELS` (or the default model) when the probe fails.
|
|
74
|
+
|
|
75
|
+
## Dev mode (vite)
|
|
76
|
+
|
|
77
|
+
In dev the frontend calls the backend **directly** (`http://127.0.0.1:8787` by
|
|
78
|
+
default) instead of going through vite's proxy — the proxy intermittently
|
|
79
|
+
truncates streaming SSE. The server answers with CORS for loopback origins
|
|
80
|
+
(`http://localhost:*` / `http://127.0.0.1:*`); override the allow-list with
|
|
81
|
+
`FLASHSTORY_CORS_ORIGINS` for non-loopback deploys. To point the dev frontend
|
|
82
|
+
at a different backend, set `VITE_FLASHSTORY_SERVER` (web/.env.development).
|
|
83
|
+
Production stays same-origin — no CORS needed.
|
|
84
|
+
|
|
85
|
+
### Browser E2E (web/e2e)
|
|
86
|
+
|
|
87
|
+
`cd web && npx playwright test e2e/agent.spec.ts` runs the AI-session chain in
|
|
88
|
+
a real Chrome (`channel: 'chrome'`, uses your system Chrome, no download)
|
|
89
|
+
against a server on `FS_E2E_BASE` (default http://127.0.0.1:8787) and a real
|
|
90
|
+
LLM. It guards the two regressions that used to break AI chat: the server's
|
|
91
|
+
defaultModel fallback (a fresh browser sends no model) and the SSE delivery of
|
|
92
|
+
error `done` events — no more "event stream closed before completion".
|
|
93
|
+
|
|
94
|
+
License: UNLICENSED — see LICENSE.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{spawn as P}from"node:child_process";import{mkdirSync as F,writeFileSync as O,readFileSync as R}from"node:fs";import{join as S}from"node:path";import{homedir as C}from"node:os";import{l as q}from"./env.js";class w extends Error{}async function N(e,t,r,i){const d=await fetch(`${e}${r}`,{method:t,headers:i!==void 0?{"content-type":"application/json"}:void 0,body:i!==void 0?JSON.stringify(i):void 0}),c=await d.json().catch(()=>null);return{status:d.status,data:c}}async function A(e,t,r){const i=await fetch(`${e}${r}`,{method:t});return{status:i.status,text:await i.text()}}async function D(e,t){const r=await fetch(`${e}${t}`);return{status:r.status,bytes:new Uint8Array(await r.arrayBuffer())}}function k(e,t){if(e.status>=200&&e.status<300)return e.data;const r=e.data?.detail??e.data?.error??`HTTP ${e.status}`;throw new w(`${t} failed: ${r}`)}const G={get:N,post:N,binary:D,text:A};function H(e){const t=[],r={};for(let i=0;i<e.length;i++){const d=e[i];if(d.startsWith("--")){const c=d.slice(2),s=e[i+1];s!==void 0&&!s.startsWith("--")?(r[c]=s,i+=1):r[c]=!0}else t.push(d)}return{positionals:t,flags:r}}function o(e,t){const r=e[t];return typeof r=="string"?r:void 0}function J(e){const t=o(e,"project");if(!t)throw new w("--project <id> is required");return t}function g(e,t){if(!e)throw new w(`--json <${t}> is required`);try{return JSON.parse(e)}catch{throw new w(`--json <${t}> is not valid JSON`)}}async function h(e,t,r,i,d=!1){const c=await e.post(t,"POST",d?"/api/tools/batch":"/api/tools/execute",{projectId:r,...d?{actions:i}:{action:i}});if(c.status===404)return{data:{error:"project not found"},exit:1};const s=c.data;return s.ok?{data:{ok:!0,rev:s.rev},exit:0}:{data:{ok:!1,error:s.error??"rejected"},exit:1}}const y=e=>e().catch(t=>({data:{error:t instanceof Error?t.message:String(t)},exit:1}));async function B(e,t,r=G){const[i,...d]=e,{positionals:c,flags:s}=H(d),l=()=>J(s);switch(i){case"health":return y(async()=>{const u=await r.get(t,"GET","/health");return{data:k(u,"health"),exit:0}});case"scene":{const u=c[0];if(u==="list")return y(async()=>{const n=await r.get(t,"GET","/api/projects");return{data:k(n,"list projects"),exit:0}});if(u==="get")return y(async()=>{const n=await r.get(t,"GET",`/api/projects/${l()}`);if(n.status===404)throw new w("project not found");return{data:n.data,exit:0}});throw f("flashstory scene list|get --project <id>")}case"entity":{const u=c[0],n=l(),a=o(s,"id"),m=o(s,"name");switch(u){case"add":return h(r,t,n,{type:"addEntity",entity:{...a?{id:a}:{},name:m??"未命名",kind:o(s,"kind")??"character",...o(s,"color")?{color:o(s,"color")}:{}}});case"remove":if(!a)throw f("--id is required");return h(r,t,n,{type:"removeEntity",id:a});case"update":{if(!a)throw f("--id is required");const x={};return m&&(x.name=m),o(s,"color")&&(x.color=o(s,"color")),o(s,"heightCm")&&(x.heightCm=Number(o(s,"heightCm"))),h(r,t,n,{type:"updateEntity",id:a,patch:x})}default:throw f("flashstory entity add|remove|update --project <id> [--id] [--name]")}}case"transform":if(c[0]!=="set"||!o(s,"id"))throw f("flashstory transform set --project --id --json <keys>");return h(r,t,l(),{type:"setTransformTrack",id:o(s,"id"),keys:g(o(s,"json"),"keys")});case"pose":{const u=c[0];if(u==="list")return y(async()=>{const n=await r.get(t,"GET",`/api/projects/${l()}`);if(n.status===404)throw new w("project not found");return{data:{customPoses:n.data.doc.customPoses??{}},exit:0}});if(u==="set-keys"){if(!o(s,"id"))throw f("--id is required");return h(r,t,l(),{type:"setPoseTrack",id:o(s,"id"),keys:g(o(s,"json"),"keys")})}throw f("flashstory pose list|set-keys --project --id --json")}case"camera":if(c[0]!=="set-keys")throw f("flashstory camera set-keys --project --json");return h(r,t,l(),{type:"setCameraTrack",keys:g(o(s,"json"),"keys")});case"settings":return h(r,t,l(),{type:"setSettings",patch:g(o(s,"json"),"patch")});case"marker":{const u=Number(o(s,"t")??NaN);if(c[0]!=="add"||!Number.isFinite(u))throw f("flashstory marker add --project --t <sec> --label <text>");return h(r,t,l(),{type:"addMarker",t:u,label:o(s,"label")??""})}case"batch":return h(r,t,l(),g(o(s,"json"),"actions"),!0);case"screenshot":return y(async()=>{const u=await r.post(t,"POST","/api/tools/screenshot",{projectId:l(),t:Number(o(s,"t")??0),camera:o(s,"camera")??"edit"}),n=k(u,"screenshot"),a=o(s,"out");if(n.kind==="image"&&a){const m=n.dataUrl?.split(",")[1];return m&&O(a,Buffer.from(m,"base64")),{data:{kind:"image",saved:a},exit:0}}return{data:n,exit:0}});case"export":{const u=c[0],n=c[1];switch(u){case"submit":return y(async()=>{const a=await r.post(t,"POST","/api/export",{projectId:l(),includeVideo:s["no-video"]!==!0,includeStills:s.stills===!0});return{data:k(a,"export submit"),exit:0}});case"status":if(!n)throw f("usage: flashstory export status <exportId>");return y(async()=>{const a=await r.get(t,"GET",`/api/export/${n}`);if(a.status===404)throw new w("export not found");return{data:a.data,exit:0}});case"result":if(!n)throw f("usage: flashstory export result <exportId> --out <dir>");return y(async()=>{const a=o(s,"out")??"exports";F(a,{recursive:!0});const x=((await r.get(t,"GET",`/api/export/${n}/files`)).data?.files??[]).filter(Boolean),$=[];for(const j of x){const E=await r.binary(t,`/api/export/${n}/files/${encodeURIComponent(j)}`);E.status===200&&(O(S(a,j),E.bytes),$.push(S(a,j)))}return{data:{exportId:n,downloaded:$},exit:0}});case"cancel":if(!n)throw f("usage: flashstory export cancel <exportId>");return y(async()=>({data:(await r.post(t,"POST",`/api/export/${n}/cancel`,{})).data,exit:0}));default:throw f("flashstory export submit|status|result|cancel")}}default:throw f(`unknown command: ${i}`)}}function f(e){return new w(e)}const L="http://127.0.0.1:8787";function U(){return S(C(),".flashstory","config.json")}function _(){try{const e=R(U(),"utf8"),t=JSON.parse(e);return typeof t.server=="string"&&t.server?t.server:void 0}catch{return}}function V(e){return e??process.env.FLASHSTORY_SERVER??_()??L}q();const v=process.argv.slice(2),p={},T=[];for(let e=0;e<v.length;e++){const t=v[e];switch(t){case"--server":p.server=v[e+1],e+=1;break;case"--port":p.port=Number(v[e+1]),e+=1;break;case"--host":p.host=v[e+1],e+=1;break;case"--data-dir":p.dataDir=v[e+1],e+=1;break;case"--open":p.open=!0;break;default:T.push(t)}}const b=T[0];if(!b||b==="serve"){const{startServer:e}=await import("./serve.js"),t={};if(p.port!==void 0&&Number.isFinite(p.port)&&(t.port=p.port),p.host&&(t.hostname=p.host),p.dataDir&&(t.dataDir=p.dataDir),await e(t),p.open){const r=`http://${t.hostname??process.env.FLASHSTORY_HOST??"127.0.0.1"}:${t.port??process.env.FLASHSTORY_PORT??8787}`;Y(r)}}else{const e=V(p.server),{data:t,exit:r}=await B(T,e);console.log(JSON.stringify(t)),process.exit(r)}function Y(e){const t=process.platform,d=P(t==="darwin"?"open":t==="win32"?"cmd":"xdg-open",t==="win32"?["/c","start","",e]:[e],{stdio:"ignore",detached:!0});d.on("error",()=>{}),d.unref?.()}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as u,statSync as h}from"node:fs";function d(n){const s={};for(const e of n.split(/\r?\n/)){const i=e.trim();if(i===""||i.startsWith("#"))continue;const o=i.startsWith("export ")?i.slice(7).trim():i,r=o.indexOf("=");if(r<=0)continue;const l=o.slice(0,r).trim();if(!l)continue;let t=o.slice(r+1).trim();t.length>=2&&(t.startsWith('"')&&t.endsWith('"')||t.startsWith("'")&&t.endsWith("'"))&&(t=t.slice(1,-1));const a=t.search(/\s#/);a>=0&&(t=t.slice(0,a).trimEnd()),s[l]=t}return s}function p(n,s){let e=0;for(const[i,o]of Object.entries(n))s[i]===void 0&&(s[i]=o,e+=1);return e}let f=!1,c=null;function x(n){f||(f=!0,v())}function v(n){const s=[void 0,process.env.FLASHSTORY_ENV_FILE,".env",E()].filter(e=>!!e);for(const e of s)if(m(e)){try{p(d(u(e,"utf8")),process.env),c=e}catch{}return}c=null}function F(){return c}function E(){const n=process.env.HOME??"";return n?`${n}/.flashstory/.env`:"~/.flashstory/.env"}function m(n){try{return h(n).isFile()}catch{return!1}}export{F as a,x as l};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{startServer as r}from"./serve.js";import{l as o}from"./env.js";import"@hono/node-server";import"node:module";import"hono";import"@hono/node-server/serve-static";import"node:fs";import"node:fs/promises";import"node:path";import"node:url";import"hono/streaming";import"stream";import"events";import"buffer";import"util";import"ws";import"node:os";o();await r();
|