Toggle navigation
Toggle navigation
This project
Loading...
Sign in
卢阳
/
front_backend_zImage
Go to a project
Toggle navigation
Projects
Groups
Snippets
Help
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
ly0303521
2026-01-08 14:00:54 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
91528abbcc9eefe319792914591e9153f80ae4fb
91528abb
1 parent
e015d921
改善视频生成页面显示,防止多次发送
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
297 additions
and
470 deletions
backend/main.py
z-image-generator/App.tsx
z-image-generator/components/HistoryBar.tsx
z-image-generator/constants.ts
z-image-generator/services/galleryService.ts
z-image-generator/services/videoService.ts
z-image-generator/types.ts
backend/main.py
View file @
91528ab
...
...
@@ -13,19 +13,20 @@ from fastapi.middleware.cors import CORSMiddleware
from
pydantic
import
BaseModel
,
Field
,
ConfigDict
import
logging
# --- Constants ---
logger
=
logging
.
getLogger
(
"uvicorn.error"
)
logging
.
basicConfig
(
level
=
logging
.
INFO
)
logger
.
info
(
"your message
%
s"
,
"hello"
)
Z_IMAGE_BASE_URL
=
os
.
getenv
(
"Z_IMAGE_BASE_URL"
,
"http://106.120.52.146:39009"
)
.
rstrip
(
"/"
)
REQUEST_TIMEOUT_SECONDS
=
float
(
os
.
getenv
(
"REQUEST_TIMEOUT_SECONDS"
,
"120"
))
GALLERY_DATA_PATH
=
Path
(
os
.
getenv
(
"GALLERY_DATA_PATH"
,
Path
(
__file__
)
.
with_name
(
"gallery_data.json"
)))
GALLERY_IMAGES_PATH
=
Path
(
os
.
getenv
(
"GALLERY_IMAGES_PATH"
,
Path
(
__file__
)
.
with_name
(
"gallery_images.json"
)))
GALLERY_VIDEOS_PATH
=
Path
(
os
.
getenv
(
"GALLERY_VIDEOS_PATH"
,
Path
(
__file__
)
.
with_name
(
"gallery_videos.json"
)))
GALLERY_MAX_ITEMS
=
int
(
os
.
getenv
(
"GALLERY_MAX_ITEMS"
,
"500"
))
WHITELIST_PATH
=
Path
(
os
.
getenv
(
"WHITELIST_PATH"
,
Path
(
__file__
)
.
with_name
(
"whitelist.txt"
)))
# --- Pydantic Models ---
# Define dependent models first to avoid forward reference issues.
class
ImageGenerationPayload
(
BaseModel
):
model_config
=
ConfigDict
(
populate_by_name
=
True
)
prompt
:
str
=
Field
(
...
,
min_length
=
1
,
max_length
=
2048
)
height
:
int
=
Field
(
1024
,
ge
=
64
,
le
=
2048
)
width
:
int
=
Field
(
1024
,
ge
=
64
,
le
=
2048
)
...
...
@@ -36,309 +37,210 @@ class ImageGenerationPayload(BaseModel):
output_format
:
Literal
[
"base64"
,
"url"
]
=
"base64"
author_id
:
Optional
[
str
]
=
Field
(
default
=
None
,
alias
=
"authorId"
,
min_length
=
1
,
max_length
=
64
)
class
ImageGenerationResponse
(
BaseModel
):
image
:
Optional
[
str
]
=
None
url
:
Optional
[
str
]
=
None
time_taken
:
float
=
0.0
error
:
Optional
[
str
]
=
None
request_params
:
ImageGenerationPayload
gallery_item
:
Optional
[
"GalleryImage"
]
=
None
class
GalleryImage
(
BaseModel
):
class
GalleryItem
(
BaseModel
):
model_config
=
ConfigDict
(
populate_by_name
=
True
)
id
:
str
prompt
:
str
=
Field
(
...
,
min_length
=
1
,
max_length
=
2048
)
height
:
int
=
Field
(
...
,
ge
=
64
,
le
=
2048
)
width
:
int
=
Field
(
...
,
ge
=
64
,
le
=
2048
)
num_inference_steps
:
int
=
Field
(
...
,
ge
=
1
,
le
=
200
)
guidance_scale
:
float
=
Field
(
...
,
ge
=
0.0
,
le
=
20.0
)
seed
:
int
=
Field
(
...
,
ge
=
0
)
url
:
str
created_at
:
float
=
Field
(
default_factory
=
lambda
:
time
.
time
()
*
1000
,
alias
=
"createdAt"
)
author_id
:
Optional
[
str
]
=
Field
(
default
=
None
,
alias
=
"authorId"
)
likes
:
int
=
0
is_mock
:
bool
=
Field
(
default
=
False
,
alias
=
"isMock"
)
negative_prompt
:
Optional
[
str
]
=
None
liked_by
:
List
[
str
]
=
Field
(
default_factory
=
list
,
alias
=
"likedBy"
)
class
GalleryImage
(
GalleryItem
):
height
:
int
=
Field
(
...
,
ge
=
64
,
le
=
2048
)
width
:
int
=
Field
(
...
,
ge
=
64
,
le
=
2048
)
num_inference_steps
:
int
=
Field
(
...
,
ge
=
1
,
le
=
200
)
guidance_scale
:
float
=
Field
(
...
,
ge
=
0.0
,
le
=
20.0
)
seed
:
int
=
Field
(
...
,
ge
=
0
)
negative_prompt
:
Optional
[
str
]
=
None
ImageGenerationResponse
.
model_rebuild
()
class
GalleryVideo
(
GalleryItem
):
generation_time
:
Optional
[
float
]
=
Field
(
default
=
None
,
alias
=
"generationTime"
)
class
ImageGenerationResponse
(
BaseModel
):
image
:
Optional
[
str
]
=
None
url
:
Optional
[
str
]
=
None
time_taken
:
float
=
0.0
error
:
Optional
[
str
]
=
None
request_params
:
ImageGenerationPayload
gallery_item
:
Optional
[
GalleryImage
]
=
None
# No forward ref needed now
class
WhitelistStore
:
"""Simple text file backed whitelist."""
# --- Data Stores ---
class
WhitelistStore
:
def
__init__
(
self
,
path
:
Path
)
->
None
:
self
.
path
=
path
self
.
lock
=
RLock
()
if
not
self
.
path
.
exists
():
# Default admin ID if file doesn't exist
self
.
_write
([
"86427531"
])
if
not
self
.
path
.
exists
():
self
.
_write
([
"86427531"
])
def
_read
(
self
)
->
List
[
str
]:
if
not
self
.
path
.
exists
():
return
[]
if
not
self
.
path
.
exists
():
return
[]
try
:
with
self
.
path
.
open
(
"r"
,
encoding
=
"utf-8"
)
as
f
:
lines
=
f
.
read
()
.
splitlines
()
return
[
line
.
strip
()
for
line
in
lines
if
line
.
strip
()]
except
OSError
:
return
[]
return
[
line
.
strip
()
for
line
in
f
.
read
()
.
splitlines
()
if
line
.
strip
()]
except
OSError
:
return
[]
def
_write
(
self
,
ids
:
List
[
str
])
->
None
:
with
self
.
lock
:
try
:
with
self
.
path
.
open
(
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
"
\n
"
.
join
(
ids
))
except
OSError
as
exc
:
print
(
f
"[WARN] Failed to write whitelist: {exc}"
)
def
is_allowed
(
self
,
user_id
:
str
)
->
bool
:
allowed
=
self
.
_read
()
return
user_id
in
allowed
with
self
.
path
.
open
(
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
"
\n
"
.
join
(
ids
))
except
OSError
as
exc
:
print
(
f
"[WARN] Failed to write whitelist: {exc}"
)
def
is_allowed
(
self
,
user_id
:
str
)
->
bool
:
return
user_id
in
self
.
_read
()
def
add_users
(
self
,
user_ids
:
List
[
str
])
->
None
:
with
self
.
lock
:
current
=
set
(
self
.
_read
())
current
.
update
(
user_ids
)
self
.
_write
(
sorted
(
list
(
current
)))
current
=
set
(
self
.
_read
());
current
.
update
(
user_ids
);
self
.
_write
(
sorted
(
list
(
current
)))
def
remove_user
(
self
,
user_id
:
str
)
->
None
:
with
self
.
lock
:
current
=
self
.
_read
()
if
user_id
in
current
:
current
=
[
uid
for
uid
in
current
if
uid
!=
user_id
]
self
.
_write
(
current
)
def
get_all
(
self
)
->
List
[
str
]:
return
self
.
_read
()
if
user_id
in
current
:
self
.
_write
([
uid
for
uid
in
current
if
uid
!=
user_id
])
def
get_all
(
self
)
->
List
[
str
]:
return
self
.
_read
()
class
GalleryStore
:
"""Simple JSON file backed store for generated images."""
def
__init__
(
self
,
path
:
Path
,
max_items
:
int
=
500
)
->
None
:
class
JsonStore
:
"""Generic JSON file backed store for a list of items."""
def
__init__
(
self
,
path
:
Path
,
item_key
:
str
,
max_items
:
int
=
500
)
->
None
:
self
.
path
=
path
self
.
item_key
=
item_key
self
.
max_items
=
max_items
self
.
lock
=
Lock
()
self
.
enabled
=
True
self
.
_memory_cache
:
List
[
dict
]
=
[]
try
:
self
.
path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
if
self
.
path
.
exists
():
self
.
_memory_cache
=
self
.
_read
()
.
get
(
"images"
,
[])
else
:
self
.
_write
({
"images"
:
[]})
except
OSError
as
exc
:
# pragma: no cover - filesystem guards
self
.
enabled
=
False
print
(
f
"[WARN] Gallery store disabled due to filesystem error: {exc}"
)
if
not
self
.
path
.
exists
():
self
.
_write
({
self
.
item_key
:
[]})
except
OSError
as
exc
:
print
(
f
"[WARN] JSON store at {path} disabled: {exc}"
)
def
_read
(
self
)
->
dict
:
if
not
self
.
enabled
:
return
{
"images"
:
list
(
self
.
_memory_cache
)}
try
:
with
self
.
path
.
open
(
"r"
,
encoding
=
"utf-8"
)
as
file
:
return
json
.
load
(
file
)
except
(
FileNotFoundError
,
json
.
JSONDecodeError
):
return
{
"images"
:
[]}
with
self
.
path
.
open
(
"r"
,
encoding
=
"utf-8"
)
as
file
:
return
json
.
load
(
file
)
except
(
FileNotFoundError
,
json
.
JSONDecodeError
):
return
{
self
.
item_key
:
[]}
def
_write
(
self
,
data
:
dict
)
->
None
:
if
not
self
.
enabled
:
self
.
_memory_cache
=
list
(
data
.
get
(
"images"
,
[]))
return
payload
=
json
.
dumps
(
data
,
ensure_ascii
=
False
,
indent
=
2
)
temp_path
=
self
.
path
.
with_suffix
(
".tmp"
)
try
:
with
temp_path
.
open
(
"w"
,
encoding
=
"utf-8"
)
as
file
:
file
.
write
(
payload
)
with
temp_path
.
open
(
"w"
,
encoding
=
"utf-8"
)
as
file
:
file
.
write
(
payload
)
temp_path
.
replace
(
self
.
path
)
except
OSError
as
exc
:
# Some filesystems (or permissions) may block atomic replace; fall back to direct write
print
(
f
"[WARN] Atomic gallery write failed, attempting direct write: {exc}"
)
try
:
with
self
.
path
.
open
(
"w"
,
encoding
=
"utf-8"
)
as
file
:
file
.
write
(
payload
)
except
OSError
as
direct_exc
:
raise
direct_exc
self
.
_memory_cache
=
list
(
data
.
get
(
"images"
,
[]))
def
list_images
(
self
)
->
List
[
dict
]:
with
self
.
lock
:
data
=
self
.
_read
()
return
list
(
data
.
get
(
"images"
,
[]))
def
add_image
(
self
,
image
:
GalleryImage
)
->
dict
:
payload
=
image
.
model_dump
(
by_alias
=
True
)
except
OSError
:
with
self
.
path
.
open
(
"w"
,
encoding
=
"utf-8"
)
as
file
:
file
.
write
(
payload
)
def
list_items
(
self
)
->
List
[
dict
]:
with
self
.
lock
:
return
self
.
_read
()
.
get
(
self
.
item_key
,
[])
def
add_item
(
self
,
item
:
BaseModel
)
->
dict
:
payload
=
item
.
model_dump
(
by_alias
=
True
)
with
self
.
lock
:
data
=
self
.
_read
()
images
=
data
.
get
(
"images"
,
[])
images
.
insert
(
0
,
payload
)
data
[
"images"
]
=
images
[:
self
.
max_items
]
items
=
data
.
get
(
self
.
item_key
,
[])
items
.
insert
(
0
,
payload
)
data
[
self
.
item_key
]
=
items
[:
self
.
max_items
]
self
.
_write
(
data
)
return
payload
def
toggle_like
(
self
,
image_id
:
str
,
user_id
:
str
)
->
Optional
[
dict
]:
def
toggle_like
(
self
,
item_id
:
str
,
user_id
:
str
)
->
Optional
[
dict
]:
with
self
.
lock
:
data
=
self
.
_read
()
images
=
data
.
get
(
"images"
,
[])
target_image
=
next
((
img
for
img
in
images
if
img
.
get
(
"id"
)
==
image_id
),
None
)
if
not
target_image
:
return
None
liked_by
=
target_image
.
get
(
"likedBy"
,
[])
# Handle legacy data where likedBy might be missing
if
not
isinstance
(
liked_by
,
list
):
liked_by
=
[]
items
=
data
.
get
(
self
.
item_key
,
[])
target_item
=
next
((
i
for
i
in
items
if
i
.
get
(
"id"
)
==
item_id
),
None
)
if
not
target_item
:
return
None
liked_by
=
target_item
.
get
(
"likedBy"
,
[])
if
not
isinstance
(
liked_by
,
list
):
liked_by
=
[]
if
user_id
in
liked_by
:
liked_by
.
remove
(
user_id
)
target_i
mage
[
"likes"
]
=
max
(
0
,
target_image
.
get
(
"likes"
,
0
)
-
1
)
target_i
tem
[
"likes"
]
=
max
(
0
,
target_item
.
get
(
"likes"
,
0
)
-
1
)
else
:
liked_by
.
append
(
user_id
)
target_image
[
"likes"
]
=
target_image
.
get
(
"likes"
,
0
)
+
1
target_image
[
"likedBy"
]
=
liked_by
target_item
[
"likes"
]
=
target_item
.
get
(
"likes"
,
0
)
+
1
target_item
[
"likedBy"
]
=
liked_by
self
.
_write
(
data
)
return
target_image
return
target_item
gallery_store
=
GalleryStore
(
GALLERY_DATA_PATH
,
GALLERY_MAX_ITEMS
)
image_store
=
JsonStore
(
GALLERY_IMAGES_PATH
,
item_key
=
"images"
,
max_items
=
GALLERY_MAX_ITEMS
)
video_store
=
JsonStore
(
GALLERY_VIDEOS_PATH
,
item_key
=
"videos"
,
max_items
=
GALLERY_MAX_ITEMS
)
whitelist_store
=
WhitelistStore
(
WHITELIST_PATH
)
# --- App Setup ---
app
=
FastAPI
(
title
=
"Z-Image Proxy"
,
version
=
"1.0.0"
)
app
.
add_middleware
(
CORSMiddleware
,
allow_origins
=
os
.
getenv
(
"ALLOWED_ORIGINS"
,
"*"
)
.
split
(
","
),
allow_origins
=
[
"http://106.120.52.146:37001"
],
# Explicitly allow the frontend origin
allow_credentials
=
True
,
allow_methods
=
[
"*"
],
allow_headers
=
[
"*"
],
)
@app.on_event
(
"startup"
)
async
def
startup
()
->
None
:
timeout
=
httpx
.
Timeout
(
REQUEST_TIMEOUT_SECONDS
,
connect
=
5.0
)
app
.
state
.
http
=
httpx
.
AsyncClient
(
timeout
=
timeout
)
async
def
startup
():
app
.
state
.
http
=
httpx
.
AsyncClient
(
timeout
=
httpx
.
Timeout
(
REQUEST_TIMEOUT_SECONDS
,
connect
=
5.0
))
@app.on_event
(
"shutdown"
)
async
def
shutdown
()
->
None
:
await
app
.
state
.
http
.
aclose
()
async
def
shutdown
():
await
app
.
state
.
http
.
aclose
()
@app.get
(
"/health"
)
async
def
health
()
->
dict
:
return
{
"status"
:
"ok"
}
async
def
health
():
return
{
"status"
:
"ok"
}
# --- Endpoints ---
@app.post
(
"/auth/login"
)
async
def
login
(
user_id
:
str
=
Query
(
...
,
alias
=
"userId"
))
->
dict
:
if
whitelist_store
.
is_allowed
(
user_id
):
return
{
"status"
:
"ok"
,
"userId"
:
user_id
}
async
def
login
(
user_id
:
str
=
Query
(
...
,
alias
=
"userId"
)):
if
whitelist_store
.
is_allowed
(
user_id
):
return
{
"status"
:
"ok"
,
"userId"
:
user_id
}
raise
HTTPException
(
status_code
=
403
,
detail
=
"User not whitelisted"
)
@app.post
(
"/likes/{item_id}"
)
async
def
toggle_like
(
item_id
:
str
,
user_id
:
str
=
Query
(
...
,
alias
=
"userId"
)):
updated_item
=
image_store
.
toggle_like
(
item_id
,
user_id
)
if
updated_item
:
return
updated_item
updated_item
=
video_store
.
toggle_like
(
item_id
,
user_id
)
if
updated_item
:
return
updated_item
raise
HTTPException
(
status_code
=
404
,
detail
=
"Item not found"
)
@app.get
(
"/gallery/images"
)
async
def
gallery_images
(
limit
:
int
=
Query
(
200
,
ge
=
1
,
le
=
1000
),
author_id
:
Optional
[
str
]
=
Query
(
None
,
alias
=
"authorId"
)):
items
=
image_store
.
list_items
()
if
author_id
:
items
=
[
item
for
item
in
items
if
item
.
get
(
"authorId"
)
==
author_id
]
return
{
"images"
:
items
[:
limit
]}
@app.get
(
"/gallery/videos"
)
async
def
gallery_videos
(
limit
:
int
=
Query
(
200
,
ge
=
1
,
le
=
1000
),
author_id
:
Optional
[
str
]
=
Query
(
None
,
alias
=
"authorId"
)):
items
=
video_store
.
list_items
()
if
author_id
:
items
=
[
item
for
item
in
items
if
item
.
get
(
"authorId"
)
==
author_id
]
return
{
"videos"
:
items
[:
limit
]}
@app.post
(
"/gallery/videos"
)
async
def
add_video
(
video
:
GalleryVideo
):
try
:
return
video_store
.
add_item
(
video
)
except
Exception
as
exc
:
raise
HTTPException
(
status_code
=
500
,
detail
=
f
"Failed to store video metadata: {exc}"
)
@app.post
(
"/generate"
,
response_model
=
ImageGenerationResponse
)
async
def
generate_image
(
payload
:
ImageGenerationPayload
):
request_params_data
=
payload
.
model_dump
();
body
=
{
k
:
v
for
k
,
v
in
request_params_data
.
items
()
if
v
is
not
None
and
k
!=
"author_id"
}
if
"seed"
not
in
body
:
body
[
"seed"
]
=
secrets
.
randbelow
(
1
_000_000_000
)
request_params_data
[
"seed"
]
=
body
[
"seed"
];
request_params
=
ImageGenerationPayload
(
**
request_params_data
)
url
=
f
"{Z_IMAGE_BASE_URL}/generate"
try
:
resp
=
await
app
.
state
.
http
.
post
(
url
,
json
=
body
)
resp
.
raise_for_status
()
data
=
resp
.
json
()
image_url
=
data
.
get
(
"url"
)
or
f
"data:image/png;base64,{data.get('image')}"
stored_item_data
=
{
"id"
:
data
.
get
(
"id"
)
or
secrets
.
token_hex
(
16
),
"prompt"
:
payload
.
prompt
,
"width"
:
payload
.
width
,
"height"
:
payload
.
height
,
"num_inference_steps"
:
payload
.
num_inference_steps
,
"guidance_scale"
:
payload
.
guidance_scale
,
"seed"
:
request_params
.
seed
,
"url"
:
image_url
,
"author_id"
:
payload
.
author_id
,
"negative_prompt"
:
payload
.
negative_prompt
,
}
stored
=
image_store
.
add_item
(
GalleryImage
(
**
stored_item_data
))
return
ImageGenerationResponse
(
image
=
data
.
get
(
"image"
),
url
=
data
.
get
(
"url"
),
time_taken
=
data
.
get
(
"time_taken"
,
0.0
),
request_params
=
request_params
,
gallery_item
=
GalleryImage
.
model_validate
(
stored
))
except
httpx
.
RequestError
as
exc
:
raise
HTTPException
(
status_code
=
502
,
detail
=
f
"Z-Image service unreachable: {exc}"
)
except
Exception
as
exc
:
raise
HTTPException
(
status_code
=
500
,
detail
=
f
"An error occurred: {exc}"
)
@app.get
(
"/admin/whitelist"
)
async
def
get_whitelist
()
->
dict
:
return
{
"whitelist"
:
whitelist_store
.
get_all
()}
@app.post
(
"/admin/whitelist"
)
async
def
add_whitelist
(
user_ids
:
List
[
str
])
->
dict
:
whitelist_store
.
add_users
(
user_ids
)
return
{
"status"
:
"ok"
,
"whitelist"
:
whitelist_store
.
get_all
()}
@app.delete
(
"/admin/whitelist/{user_id}"
)
async
def
remove_whitelist
(
user_id
:
str
)
->
dict
:
whitelist_store
.
remove_user
(
user_id
)
return
{
"status"
:
"ok"
,
"whitelist"
:
whitelist_store
.
get_all
()}
@app.post
(
"/likes/{image_id}"
)
async
def
toggle_like
(
image_id
:
str
,
user_id
:
str
=
Query
(
...
,
alias
=
"userId"
)
)
->
dict
:
"""Toggle like status for an image by a user."""
updated_image
=
gallery_store
.
toggle_like
(
image_id
,
user_id
)
if
not
updated_image
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"Image not found"
)
return
updated_image
# Redirect old /gallery to /gallery/images for backward compatibility
@app.get
(
"/gallery"
)
async
def
gallery
(
limit
:
int
=
Query
(
200
,
ge
=
1
,
le
=
1000
),
author_id
:
Optional
[
str
]
=
Query
(
default
=
None
,
alias
=
"authorId"
),
)
->
dict
:
"""Return the persisted gallery images, optionally filtered by author."""
images
=
gallery_store
.
list_images
()
if
author_id
:
images
=
[
item
for
item
in
images
if
item
.
get
(
"authorId"
)
==
author_id
]
return
{
"images"
:
images
[:
limit
]}
@app.post
(
"/generate"
,
response_model
=
ImageGenerationResponse
)
async
def
generate_image
(
payload
:
ImageGenerationPayload
)
->
ImageGenerationResponse
:
request_params_data
=
payload
.
model_dump
()
body
=
{
key
:
value
for
key
,
value
in
request_params_data
.
items
()
if
value
is
not
None
and
key
!=
"author_id"
}
if
"seed"
not
in
body
:
body
[
"seed"
]
=
secrets
.
randbelow
(
1
_000_000_000
)
request_params_data
[
"seed"
]
=
body
[
"seed"
]
request_params
=
ImageGenerationPayload
(
**
request_params_data
)
url
=
f
"{Z_IMAGE_BASE_URL}/generate"
try
:
resp
=
await
app
.
state
.
http
.
post
(
url
,
json
=
body
)
except
httpx
.
RequestError
as
exc
:
# pragma: no cover - network errors only
raise
HTTPException
(
status_code
=
502
,
detail
=
f
"Z-Image service unreachable: {exc}"
)
from
exc
if
resp
.
status_code
!=
200
:
raise
HTTPException
(
status_code
=
resp
.
status_code
,
detail
=
f
"Z-Image error: {resp.text}"
)
data
=
resp
.
json
()
image
=
data
.
get
(
"image"
)
image_url
=
data
.
get
(
"url"
)
if
not
image
and
not
image_url
:
raise
HTTPException
(
status_code
=
502
,
detail
=
f
"Malformed response from Z-Image: {data}"
)
stored_image
:
Optional
[
GalleryImage
]
=
None
try
:
stored
=
gallery_store
.
add_image
(
GalleryImage
(
id
=
data
.
get
(
"id"
)
or
secrets
.
token_hex
(
16
),
prompt
=
payload
.
prompt
,
width
=
payload
.
width
,
height
=
payload
.
height
,
num_inference_steps
=
payload
.
num_inference_steps
,
guidance_scale
=
payload
.
guidance_scale
,
seed
=
request_params
.
seed
,
url
=
image_url
or
f
"data:image/png;base64,{image}"
,
author_id
=
payload
.
author_id
,
negative_prompt
=
payload
.
negative_prompt
,
)
)
stored_image
=
GalleryImage
.
model_validate
(
stored
)
except
Exception
as
exc
:
# pragma: no cover - diagnostics only
# Persisting gallery data should not block the response
print
(
f
"[WARN] Failed to store gallery image: {exc}"
)
return
ImageGenerationResponse
(
image
=
image
,
url
=
image_url
,
time_taken
=
float
(
data
.
get
(
"time_taken"
,
0.0
)),
error
=
data
.
get
(
"error"
),
request_params
=
request_params
,
gallery_item
=
stored_image
,
)
async
def
gallery
(
limit
:
int
=
Query
(
200
,
ge
=
1
,
le
=
1000
),
author_id
:
Optional
[
str
]
=
Query
(
None
,
alias
=
"authorId"
)):
return
await
gallery_images
(
limit
=
limit
,
author_id
=
author_id
)
\ No newline at end of file
...
...
z-image-generator/App.tsx
View file @
91528ab
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import { ImageItem, ImageGenerationParams, UserProfile, VideoStatus } from './types';
import { SHOWCASE_IMAGES, ADMIN_ID } from './constants';
import { SHOWCASE_IMAGES, ADMIN_ID
, VIDEO_OSS_BASE_URL
} from './constants';
import { generateImage } from './services/imageService';
import { submitVideoJob, pollVideoStatus, getVideoResultUrl } from './services/videoService';
import { fetchGallery, toggleLike } from './services/galleryService';
import { submitVideoJob, pollVideoStatus } from './services/videoService';
import { fetchGallery, fetchVideoGallery, saveVideo, toggleLike } from './services/galleryService';
import MasonryGrid from './components/MasonryGrid';
import InputBar from './components/InputBar';
import HistoryBar from './components/HistoryBar';
...
...
@@ -13,93 +13,86 @@ import AuthModal from './components/AuthModal';
import WhitelistModal from './components/WhitelistModal';
import { Loader2, Trash2, User as UserIcon, Save, Settings, Sparkles, Users, Video, Hourglass } from 'lucide-react';
const STORAGE_KEY_DATA = 'z-image-gallery-data-v2';
const STORAGE_KEY_VIDEO_DATA = 'z-video-gallery-data-v1';
const STORAGE_KEY_USER = 'z-image-user-profile';
const MIN_GALLERY_ITEMS = 8;
// --- Enums and Types ---
enum GalleryMode {
Image,
Video,
}
const App: React.FC = () => {
//
--- State: User ---
//
State
const [currentUser, setCurrentUser] = useState<UserProfile | null>(null);
const [isAuthModalOpen, setIsAuthModalOpen] = useState(false);
const [isWhitelistModalOpen, setIsWhitelistModalOpen] = useState(false);
// --- State: UI ---
const [galleryMode, setGalleryMode] = useState<GalleryMode>(GalleryMode.Image);
// --- State: Data ---
const [images, setImages] = useState<ImageItem[]>(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY_DATA);
if (saved) return JSON.parse(saved);
} catch (e) { console.error(e); }
return SHOWCASE_IMAGES;
});
const [videos, setVideos] = useState<ImageItem[]>(() => {
try {
const saved = localStorage.getItem(STORAGE_KEY_VIDEO_DATA);
if (saved) return JSON.parse(saved);
} catch(e) { console.error(e); }
return [];
});
const [images, setImages] = useState<ImageItem[]>(SHOWCASE_IMAGES);
const [videos, setVideos] = useState<ImageItem[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [videoStatus, setVideoStatus] = useState<VideoStatus | null>(null);
const [selectedImage, setSelectedImage] = useState<ImageItem | null>(null);
const [error, setError] = useState<string | null>(null);
const [incomingParams, setIncomingParams] = useState<ImageGenerationParams | null>(null);
// --- State: Admin/Edit ---
const [isAdminModalOpen, setIsAdminModalOpen] = useState(false);
const [isWhitelistModalOpen, setIsWhitelistModalOpen] = useState(false);
const [editingImage, setEditingImage] = useState<ImageItem | null>(null);
const isAdmin = currentUser?.employeeId === ADMIN_ID;
const isGeneratingVideo = videoStatus !== null;
// GLOBAL GALLERY: Everyone sees everything, sorted by likes
const sortedImages = useMemo(() => {
return [...images].sort((a, b) => (b.likes || 0) - (a.likes || 0));
}, [images]);
// USER HISTORY: Only current user's generations
const sortedImages = useMemo(() => [...images].sort((a, b) => (b.likes || 0) - (a.likes || 0)), [images]);
const sortedVideos = useMemo(() => [...videos].sort((a, b) => b.createdAt - a.createdAt), [videos]);
const userHistory = useMemo(() => {
if (!currentUser) return [];
return images
.filter(img => img.authorId === currentUser.employeeId)
.sort((a, b) => b.createdAt - a.createdAt);
return images.filter(img => img.authorId === currentUser.employeeId).sort((a, b) => b.createdAt - a.createdAt);
}, [images, currentUser]);
const userVideoHistory = useMemo(() => {
if (!currentUser) return [];
return videos
.filter(vid => vid.authorId === currentUser.employeeId)
.sort((a, b) => b.createdAt - a.createdAt);
return videos.filter(vid => vid.authorId === currentUser.employeeId).sort((a, b) => b.createdAt - a.createdAt);
}, [videos, currentUser]);
// --- Auth Effect ---
useEffect(() => {
const savedUser = localStorage.getItem(STORAGE_KEY_USER);
if (savedUser) setCurrentUser(JSON.parse(savedUser));
else setIsAuthModalOpen(true);
}, []);
useEffect(() => {
try { localStorage.setItem(STORAGE_KEY_DATA, JSON.stringify(images)); }
catch (e) { console.error("Storage full", e); }
}, [images]);
// --- Data Sync ---
const syncImageGallery = useCallback(async () => {
try {
const remoteImages = await fetchGallery();
setImages(prev => {
const normalized = remoteImages.map(img => ({ ...img, isLikedByCurrentUser: currentUser ? (img.likedBy || []).includes(currentUser.employeeId) : false }));
if (normalized.length >= MIN_GALLERY_ITEMS) return normalized;
const existingIds = new Set(normalized.map(img => img.id));
const filler = SHOWCASE_IMAGES.filter(img => !existingIds.has(img.id)).slice(0, MIN_GALLERY_ITEMS - normalized.length);
return [...normalized, ...filler];
});
} catch (err) { console.error("Failed to sync image gallery", err); }
}, [currentUser]);
const syncVideoGallery = useCallback(async () => {
try {
const remoteVideos = await fetchVideoGallery();
setVideos(remoteVideos.map(vid => ({ ...vid, isLikedByCurrentUser: currentUser ? (vid.likedBy || []).includes(currentUser.employeeId) : false })));
} catch (err) { console.error("Failed to sync video gallery", err); }
}, [currentUser]);
useEffect(() => {
try { localStorage.setItem(STORAGE_KEY_VIDEO_DATA, JSON.stringify(videos)); }
catch (e) { console.error("Storage full for videos", e); }
}, [videos]);
syncImageGallery();
syncVideoGallery();
const interval = setInterval(() => {
syncImageGallery();
syncVideoGallery();
}, 30000);
return () => clearInterval(interval);
}, [syncImageGallery, syncVideoGallery]);
// --- Handlers ---
const handleLogin = (employeeId: string) => {
const user: UserProfile = { employeeId, hasAccess: true };
setCurrentUser(user);
...
...
@@ -108,73 +101,29 @@ const App: React.FC = () => {
};
const handleLogout = () => {
if(confirm("确定要退出登录吗?")) {
if
(confirm("确定要退出登录吗?")) {
localStorage.removeItem(STORAGE_KEY_USER);
setCurrentUser(null);
setIsAuthModalOpen(true);
}
};
const syncGallery = useCallback(async () => {
if (galleryMode === GalleryMode.Video) {
// We are not syncing videos from a central gallery in this version.
return;
}
try {
const remoteImages = await fetchGallery();
setImages(prev => {
const normalized = remoteImages.map(img => {
const isLiked = img.likedBy && currentUser
? img.likedBy.includes(currentUser.employeeId)
: false;
return {
...img,
likes: img.likes, // Trust server
isLikedByCurrentUser: isLiked,
};
});
if (normalized.length >= MIN_GALLERY_ITEMS) {
return normalized;
}
const existingIds = new Set(normalized.map(img => img.id));
const filler = SHOWCASE_IMAGES.filter(img => !existingIds.has(img.id)).slice(
0,
Math.max(0, MIN_GALLERY_ITEMS - normalized.length)
);
return [...normalized, ...filler];
});
} catch (err) {
console.error("Failed to sync gallery", err);
}
}, [currentUser, galleryMode]);
useEffect(() => {
syncGallery();
const interval = setInterval(syncGallery, 30000);
return () => clearInterval(interval);
}, [syncGallery]);
const handleGenerateVideo = async (params: ImageGenerationParams, imageFile: File) => {
if (!currentUser) {
setIsAuthModalOpen(true);
return;
}
if (!currentUser) { setIsAuthModalOpen(true); return; }
setError(null);
setVideoStatus({ status: 'submitting', message: '提交中...', task_id: 'temp' });
try {
const taskId = await submitVideoJob(params.prompt, imageFile);
const finalStatus = await pollVideoStatus(taskId, (statusUpdate) => {
setVideoStatus(statusUpdate);
});
const taskId = await submitVideoJob(params.prompt, imageFile, currentUser.employeeId);
const finalStatus = await pollVideoStatus(taskId, setVideoStatus);
const newVideo: ImageItem = {
if (!finalStatus.video_filename) {
throw new Error("视频生成完成,但未找到有效的视频文件名。");
}
const newVideoData: ImageItem = {
id: `vid-${Date.now()}`,
url:
getVideoResultUrl(taskId)
,
url:
`${VIDEO_OSS_BASE_URL}/${finalStatus.video_filename}`
,
prompt: params.prompt,
authorId: currentUser.employeeId,
createdAt: Date.now(),
...
...
@@ -182,63 +131,34 @@ const App: React.FC = () => {
isLikedByCurrentUser: false,
generationTime: finalStatus.processing_time,
};
setVideos(prev => [newVideo, ...prev]);
// Keep the "complete" status visible for a few seconds before clearing
setTimeout(() => {
setVideoStatus(null);
}, 3000); // Display for 3 seconds
const savedVideo = await saveVideo(newVideoData);
setVideos(prev => [savedVideo, ...prev]);
setTimeout(() => setVideoStatus(null), 3000);
} catch (err: any) {
console.error(err);
setError("视频生成失败。请确保视频生成服务正常运行。");
setVideoStatus(null);
// Clear immediately on error
setVideoStatus(null);
}
};
const handleGenerate = async (uiParams: ImageGenerationParams, imageFile?: File) => {
if (galleryMode === GalleryMode.Video) {
if (!imageFile) {
alert("请上传一张图片以生成视频。");
return;
}
handleGenerateVideo(uiParams, imageFile);
return;
}
if (!currentUser) {
setIsAuthModalOpen(true);
if (!imageFile) { alert("请上传一张图片以生成视频。"); return; }
handleGenerateVideo(uiParams, imageFile);
return;
}
if (!currentUser) { setIsAuthModalOpen(true); return; }
setIsGenerating(true);
setError(null);
setIncomingParams(null); // Reset syncing state once action starts
setIncomingParams(null);
try {
const result = await generateImage(uiParams, currentUser.employeeId);
const serverImage = result.galleryItem
? { ...result.galleryItem, isLikedByCurrentUser: false }
: null;
const newImage: ImageItem = serverImage || {
id: Date.now().toString(),
url: result.imageUrl,
createdAt: Date.now(),
authorId: currentUser.employeeId,
likes: 0,
isLikedByCurrentUser: false,
...uiParams
};
setImages(prev => {
const existing = prev.filter(img => img.id !== newImage.id);
return [newImage, ...existing];
});
if (!serverImage) {
await syncGallery();
}
const serverImage = result.galleryItem ? { ...result.galleryItem, isLikedByCurrentUser: false } : null;
const newImage: ImageItem = serverImage || { id: Date.now().toString(), url: result.imageUrl, createdAt: Date.now(), authorId: currentUser.employeeId, likes: 0, isLikedByCurrentUser: false, ...uiParams };
setImages(prev => [newImage, ...prev.filter(img => img.id !== newImage.id)]);
if (!serverImage) await syncImageGallery();
} catch (err: any) {
console.error(err);
setError("生成失败。请确保服务器正常运行。");
...
...
@@ -247,77 +167,55 @@ const App: React.FC = () => {
}
};
const handleLike = async (image: ImageItem) => {
if (!currentUser) {
setIsAuthModalOpen(true);
return;
}
const handleLike = async (item: ImageItem) => {
if (!currentUser) { setIsAuthModalOpen(true); return; }
const isVideo = item.id.startsWith('vid-');
const stateSetter = isVideo ? setVideos : setImages;
// Optimistic update
const previousImages = [...images];
setImages(prev => prev.map(img => {
if (img.id === image.id) {
const isLiked = !!img.isLikedByCurrentUser;
return {
...img,
isLikedByCurrentUser: !isLiked,
likes: isLiked ? Math.max(0, (img.likes || 0) - 1) : (img.likes || 0) + 1
};
stateSetter(prev => prev.map(i => {
if (i.id === item.id) {
const isLiked = !!i.isLikedByCurrentUser;
return { ...i, isLikedByCurrentUser: !isLiked, likes: (i.likes || 0) + (isLiked ? -1 : 1) };
}
return i
mg
;
return i;
}));
try {
await toggleLike(image
.id, currentUser.employeeId);
await toggleLike(item
.id, currentUser.employeeId);
} catch (e) {
console.error("Like failed", e);
setImages(previousImages); // Revert
alert("操作失败");
console.error("Like failed", e);
if (isVideo) syncVideoGallery(); else syncImageGallery();
alert("操作失败");
}
};
const handleGenerateSimilar = (params: ImageGenerationParams) => {
setIncomingParams(params);
const banner = document.getElementById('similar-feedback');
if (banner) {
banner.style.display = 'flex';
setTimeout(() => { banner.style.display = 'none'; }, 3000);
}
if (banner) { banner.style.display = 'flex'; setTimeout(() => { banner.style.display = 'none'; }, 3000); }
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
};
// --- Management (ADMIN) ---
// Admin handlers...
const handleOpenCreateModal = () => { if (isAdmin) { setEditingImage(null); setIsAdminModalOpen(true); } };
const handleOpenEditModal = (image: ImageItem) => { if (isAdmin) { setEditingImage(image); setSelectedImage(null); setIsAdminModalOpen(true); } };
const handleSaveImage = (savedImage: ImageItem) => {
setImages(prev => {
const exists = prev.some(img => img.id === savedImage.id);
if (exists) return prev.map(img => img.id === savedImage.id ? savedImage : img);
return [{ ...savedImage, authorId: currentUser?.employeeId || 'ADMIN', likes: savedImage.likes || 0 }, ...prev];
});
};
const handleSaveImage = (savedImage: ImageItem) => setImages(prev => { const exists = prev.some(img => img.id === savedImage.id); if (exists) return prev.map(img => img.id === savedImage.id ? savedImage : img); return [{ ...savedImage, authorId: currentUser?.employeeId || 'ADMIN', likes: savedImage.likes || 0 }, ...prev]; });
const handleDeleteImage = (id: string) => { if (isAdmin) setImages(prev => prev.filter(img => img.id !== id)); };
const handleResetData = () => { if (isAdmin && confirm('警告:确定要重置为初始演示数据吗?')) { setImages(SHOWCASE_IMAGES); } };
const handleExportShowcase = () => {
const exportData = images.map(img => ({ ...img, isLikedByCurrentUser: false, isMock: true }));
const fileContent = `
...`; // Omitted for brevity
const fileContent = `
import { ImageItem } from './types';\n\nexport const SHOWCASE_IMAGES: ImageItem[] = ${JSON.stringify(exportData, null, 2)};`;
const blob = new Blob([fileContent], { type: 'text/typescript' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = '
constant
s.ts';
a.download = '
showcase_image
s.ts';
a.click();
};
return (
<div className="min-h-screen bg-white text-gray-900 font-sans pb-40">
<AuthModal isOpen={isAuthModalOpen} onLogin={handleLogin} />
<div id="similar-feedback" className="fixed top-24 left-1/2 -translate-x-1/2 z-[60] bg-purple-600 text-white px-6 py-2 rounded-full shadow-lg hidden items-center gap-2 animate-bounce">
<Sparkles size={16} />
<span className="text-sm font-bold tracking-wide">参数已同步到输入框</span>
</div>
<header className="px-6 py-6 md:px-12 md:py-8 flex justify-between items-end sticky top-0 z-40 bg-white/80 backdrop-blur-md border-b border-gray-100">
<div>
<h1 className="text-3xl md:text-4xl font-extrabold tracking-tight mb-2">艺云-DESIGN</h1>
...
...
@@ -348,52 +246,27 @@ const App: React.FC = () => {
</header>
<div className="px-6 md:px-12 mt-6 mb-4 flex border-b">
<button
onClick={() => setGalleryMode(GalleryMode.Image)}
className={`px-4 py-2 font-medium text-sm transition-colors ${galleryMode === GalleryMode.Image ? 'border-b-2 border-purple-600 text-purple-600' : 'text-gray-500 hover:text-gray-800'}`}
>
灵感图库
</button>
<button
onClick={() => setGalleryMode(GalleryMode.Video)}
className={`px-4 py-2 font-medium text-sm transition-colors ${galleryMode === GalleryMode.Video ? 'border-b-2 border-purple-600 text-purple-600' : 'text-gray-500 hover:text-gray-800'}`}
>
视频素材
</button>
<button onClick={() => setGalleryMode(GalleryMode.Image)} className={`px-4 py-2 font-medium text-sm transition-colors ${galleryMode === GalleryMode.Image ? 'border-b-2 border-purple-600 text-purple-600' : 'text-gray-500 hover:text-gray-800'}`}>灵感图库</button>
<button onClick={() => setGalleryMode(GalleryMode.Video)} className={`px-4 py-2 font-medium text-sm transition-colors ${galleryMode === GalleryMode.Video ? 'border-b-2 border-purple-600 text-purple-600' : 'text-gray-500 hover:text-gray-800'}`}>视频素材</button>
</div>
{error && <div className="mx-6 md:mx-12 mt-4 p-4 bg-red-50 border border-red-200 text-red-600 rounded-lg text-sm flex justify-between"><span>{error}</span><button onClick={() => setError(null)}><Trash2 size={14}/></button></div>}
<main>
{isGenerating && ( // This is for image generation only
<div className="w-full flex justify-center py-12">
<div className="flex flex-col items-center animate-pulse">
<Loader2 className="animate-spin text-purple-600 mb-3" size={40} />
<span className="text-gray-500 font-medium">绘图引擎全力启动中...</span>
</div>
</div>
{isGenerating && (
<div className="w-full flex justify-center py-12"><div className="flex flex-col items-center animate-pulse"><Loader2 className="animate-spin text-purple-600 mb-3" size={40} /><span className="text-gray-500 font-medium">绘图引擎全力启动中...</span></div></div>
)}
{galleryMode === GalleryMode.Image ? (
<MasonryGrid images={sortedImages} onImageClick={setSelectedImage} onLike={handleLike} currentUser={currentUser?.employeeId}/>
) : (
<MasonryGrid images={
videos} onImageClick={setSelectedImage} onLike={() => {}
} currentUser={currentUser?.employeeId} isVideoGallery={true} />
<MasonryGrid images={
sortedVideos} onImageClick={setSelectedImage} onLike={handleLike
} currentUser={currentUser?.employeeId} isVideoGallery={true} />
)}
</main>
<HistoryBar images={galleryMode === GalleryMode.Image ? userHistory : userVideoHistory} onSelect={setSelectedImage} />
<InputBar onGenerate={handleGenerate} isGenerating={isGenerating || isGeneratingVideo} incomingParams={incomingParams} isVideoMode={galleryMode === GalleryMode.Video} videoStatus={videoStatus} />
{selectedImage && (
<DetailModal
image={selectedImage}
onClose={() => setSelectedImage(null)}
onEdit={isAdmin ? handleOpenEditModal : undefined}
onGenerateSimilar={handleGenerateSimilar}
/>
)}
{selectedImage && <DetailModal image={selectedImage} onClose={() => setSelectedImage(null)} onEdit={isAdmin ? handleOpenEditModal : undefined} onGenerateSimilar={handleGenerateSimilar}/>}
<AdminModal isOpen={isAdminModalOpen} onClose={() => setIsAdminModalOpen(false)} onSave={handleSaveImage} onDelete={handleDeleteImage} initialData={editingImage} />
<WhitelistModal isOpen={isWhitelistModalOpen} onClose={() => setIsWhitelistModalOpen(false)} />
</div>
...
...
z-image-generator/components/HistoryBar.tsx
View file @
91528ab
...
...
@@ -23,12 +23,22 @@ const HistoryBar: React.FC<HistoryBarProps> = ({ images, onSelect }) => {
onClick={() => onSelect(img)}
className="flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden border-2 border-white dark:border-gray-800 shadow-sm hover:scale-105 hover:border-purple-400 transition-all"
>
<img
src={img.url}
alt="History"
className="w-full h-full object-cover"
loading="lazy"
/>
{img.id.startsWith('vid-') ? (
<video
src={img.url}
className="w-full h-full object-cover"
muted
playsInline
preload="metadata"
/>
) : (
<img
src={img.url}
alt="History"
className="w-full h-full object-cover"
loading="lazy"
/>
)}
</button>
))}
</div>
...
...
z-image-generator/constants.ts
View file @
91528ab
import
{
ImageItem
}
from
'./types'
;
export
const
Z_IMAGE_DIRECT_BASE_URL
=
"http://106.120.52.146:39009"
;
export
const
TURBO_DIFFUSION_VIDEO_BASE_URL
=
"http://106.120.52.146:38000"
;
// Base URL for the TurboDiffusion AI service (submit job, poll status)
export
const
TURBO_DIFFUSION_API_URL
=
"http://106.120.52.146:38000"
;
// Base URL for the OSS service that serves the final video files
export
const
VIDEO_OSS_BASE_URL
=
"http://106.120.52.146:39997"
;
const
ENV_PROXY_URL
=
import
.
meta
.
env
?.
VITE_API_BASE_URL
?.
trim
();
const
DEFAULT_PROXY_URL
=
ENV_PROXY_URL
&&
ENV_PROXY_URL
.
length
>
0
...
...
z-image-generator/services/galleryService.ts
View file @
91528ab
...
...
@@ -10,22 +10,65 @@ export const fetchGallery = async (authorId?: string): Promise<ImageItem[]> => {
params
.
set
(
'authorId'
,
authorId
);
}
const
query
=
params
.
toString
();
const
response
=
await
fetch
(
`
$
{
API_BASE_URL
}
/gallery${query
?
`
?
${query}` : ''}`
)
;
// Note: The backend endpoint was changed from /gallery to /gallery/images
const
response
=
await
fetch
(
`
$
{
API_BASE_URL
}
/gallery/im
ages$
{
query
?
`
?
$
{
query
}
`
:
''
}
`
);
if
(
!
response
.
ok
)
{
const
errorText
=
await
response
.
text
();
throw
new
Error
(
`
G
allery
fetch
failed
(
$
{
response
.
status
})
:
$
{
errorText
}
`
);
throw
new
Error
(
`
Image
g
allery
fetch
failed
(
$
{
response
.
status
}):
$
{
errorText
}
`
);
}
const
data
:
GalleryResponse
=
await
response
.
json
();
return
data
.
images
??
[];
};
export
const
toggleLike
=
async
(
imageId
:
string
,
userId
:
string
)
:
Promise
<
ImageItem
>
=>
{
export
const
fetchVideoGallery
=
async
(
authorId
?:
string
):
Promise
<
ImageItem
[]
>
=>
{
if
(
API_BASE_URL
===
Z_IMAGE_DIRECT_BASE_URL
)
{
throw
new
Error
(
"Cannot like images in direct mode"
);
return
[];
}
const
params
=
new
URLSearchParams
();
if
(
authorId
)
{
params
.
set
(
'authorId'
,
authorId
);
}
const
query
=
params
.
toString
();
const
response
=
await
fetch
(
`
$
{
API_BASE_URL
}
/gallery/
videos$
{
query
?
`
?
$
{
query
}
`
:
''
}
`
);
if
(
!
response
.
ok
)
{
const
errorText
=
await
response
.
text
();
throw
new
Error
(
`
Video
gallery
fetch
failed
(
$
{
response
.
status
}):
$
{
errorText
}
`
);
}
// Assuming the response for videos is { videos: ImageItem[] }
const
data
:
{
videos
:
ImageItem
[]
}
=
await
response
.
json
();
return
data
.
videos
??
[];
};
export
const
saveVideo
=
async
(
videoData
:
ImageItem
):
Promise
<
ImageItem
>
=>
{
if
(
API_BASE_URL
===
Z_IMAGE_DIRECT_BASE_URL
)
{
throw
new
Error
(
"Cannot save videos in direct mode"
);
}
const
response
=
await
fetch
(
`
$
{
API_BASE_URL
}
/gallery/
videos
`
,
{
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
},
body
:
JSON
.
stringify
(
videoData
),
});
if
(
!
response
.
ok
)
{
const
errorText
=
await
response
.
text
();
throw
new
Error
(
`
Save
video
failed
(
$
{
response
.
status
})
:
$
{
errorText
}
`
);
}
const
response
=
await
fetch
(
`
$
{
API_BASE_URL
}
/likes/
$
{
imageId
}?
userId
=
$
{
userId
}
`
,
{
return
await
response
.
json
();
};
export
const
toggleLike
=
async
(
itemId
:
string
,
userId
:
string
)
:
Promise
<
ImageItem
>
=>
{
if
(
API_BASE_URL
===
Z_IMAGE_DIRECT_BASE_URL
)
{
throw
new
Error
(
"Cannot like items in direct mode"
);
}
const
response
=
await
fetch
(
`
$
{
API_BASE_URL
}
/likes/
$
{
itemId
}?
userId
=
$
{
userId
}
`
,
{
method
:
'POST'
,
});
...
...
@@ -35,4 +78,4 @@ export const toggleLike = async (imageId: string, userId: string): Promise<Image
}
return
await
response
.
json
();
};
};
\ No newline at end of file
...
...
z-image-generator/services/videoService.ts
View file @
91528ab
import
{
TURBO_DIFFUSION_
VIDEO_BASE
_URL
}
from
'../constants'
;
import
{
TURBO_DIFFUSION_
API
_URL
}
from
'../constants'
;
import
{
VideoStatus
}
from
'../types'
;
/**
* Submits a video generation job to the backend.
* @returns The task ID for the submitted job.
*/
export
const
submitVideoJob
=
async
(
prompt
:
string
,
image
:
File
)
:
Promise
<
string
>
=>
{
export
const
submitVideoJob
=
async
(
prompt
:
string
,
image
:
File
,
authorId
:
string
)
:
Promise
<
string
>
=>
{
const
formData
=
new
FormData
();
formData
.
append
(
'prompt'
,
prompt
);
formData
.
append
(
'image'
,
image
,
image
.
name
);
formData
.
append
(
'author_id'
,
authorId
);
const
submitRes
=
await
fetch
(
`
$
{
TURBO_DIFFUSION_
VIDEO_BASE
_URL
}
/submit-job/
`
,
{
const
submitRes
=
await
fetch
(
`
$
{
TURBO_DIFFUSION_
API
_URL
}
/submit-job/
`
,
{
method
:
'POST'
,
body
:
formData
,
});
...
...
@@ -37,7 +38,7 @@ export const pollVideoStatus = (
return
new
Promise
((
resolve
,
reject
)
=>
{
const
interval
=
setInterval
(
async
()
=>
{
try
{
const
res
=
await
fetch
(
`
$
{
TURBO_DIFFUSION_
VIDEO_BASE
_URL
}
/status/
$
{
taskId
}
`
);
const
res
=
await
fetch
(
`
$
{
TURBO_DIFFUSION_
API
_URL
}
/status/
$
{
taskId
}
`
);
if
(
!
res
.
ok
)
{
// Stop polling on HTTP error
clearInterval
(
interval
);
...
...
@@ -63,12 +64,4 @@ export const pollVideoStatus = (
}
},
2000
);
// Poll every 2 seconds
});
};
/**
* Gets the final URL for a completed video task.
*/
export
const
getVideoResultUrl
=
(
taskId
:
string
):
string
=>
{
return
`
$
{
TURBO_DIFFUSION_VIDEO_BASE_URL
}
/result/
$
{
taskId
}
`
;
};
\ No newline at end of file
...
...
z-image-generator/types.ts
View file @
91528ab
...
...
@@ -48,4 +48,5 @@ export interface VideoStatus {
message
:
string
;
queue_position
?:
number
;
processing_time
?:
number
;
video_filename
?:
string
;
// The final filename of the video
}
...
...
Please
register
or
login
to post a comment