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-30 15:19:17 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
efa4823d6421749d3a7c99a1c656cd2ee66cfd13
efa4823d
1 parent
e9cd28f6
视频加载使用缩略图
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
78 additions
and
9 deletions
backend/main.py
start_all.sh
z-image-generator/App.tsx
z-image-generator/components/ImageCard.tsx
z-image-generator/services/videoService.ts
z-image-generator/types.ts
backend/main.py
View file @
efa4823
...
...
@@ -14,6 +14,8 @@ from fastapi import FastAPI, HTTPException, Query, UploadFile, File, Form
from
fastapi.middleware.cors
import
CORSMiddleware
from
pydantic
import
BaseModel
,
Field
,
ConfigDict
import
logging
from
PIL
import
Image
import
io
# --- Constants ---
logger
=
logging
.
getLogger
(
"uvicorn.error"
)
...
...
@@ -175,6 +177,7 @@ class GalleryVideo(GalleryItem):
seed
:
Optional
[
int
]
=
Field
(
default
=
None
,
ge
=
0
)
width
:
int
=
Field
(
1024
,
ge
=
64
,
le
=
2048
)
height
:
int
=
Field
(
1024
,
ge
=
64
,
le
=
2048
)
thumbnail
:
Optional
[
str
]
=
None
class
ImageGenerationResponse
(
BaseModel
):
image
:
Optional
[
str
]
=
None
...
...
@@ -310,6 +313,10 @@ usage_store = UsageStore(USAGE_PATH)
# --- App Setup ---
app
=
FastAPI
(
title
=
"Z-Image Proxy"
,
version
=
"1.0.0"
)
from
fastapi.staticfiles
import
StaticFiles
# Mount public directory to serve thumbnails and frontend config
app
.
mount
(
"/thumbnails"
,
StaticFiles
(
directory
=
str
(
Path
(
__file__
)
.
parent
.
parent
/
"public"
/
"thumbnails"
)),
name
=
"thumbnails"
)
app
.
add_middleware
(
CORSMiddleware
,
allow_origins
=
[
"*"
],
...
...
@@ -402,6 +409,46 @@ async def submit_video_job_proxy(
# Prepare files and data for forwarding
file_content
=
await
image
.
read
()
# --- Save Thumbnail ---
# Generate a lightweight thumbnail (max 400px width/height, ~20KB)
thumbnail_filename
=
f
"thumb_{secrets.token_hex(8)}.jpg"
# Always use jpg for efficiency
# Updated Path: Save to OSS directory
OSS_THUMBNAIL_DIR
=
Path
(
"/home/inspur/work_space/gen_img_video/TurboDiffusion-Space/ASSERT/艺云-DESIGN/thumbnails"
)
thumbnail_path
=
OSS_THUMBNAIL_DIR
/
thumbnail_filename
# Updated URL: Construct from environment variables (PUBLIC_IP and PUBLIC_OSS_PORT)
public_ip
=
os
.
getenv
(
"PUBLIC_IP"
,
"106.120.52.146"
)
oss_port
=
os
.
getenv
(
"PUBLIC_OSS_PORT"
,
"34000"
)
oss_base_url
=
f
"http://{public_ip}:{oss_port}"
thumbnail_url_path
=
f
"{oss_base_url}/thumbnails/{thumbnail_filename}"
try
:
thumbnail_path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
# Use PIL to resize and compress
with
Image
.
open
(
io
.
BytesIO
(
file_content
))
as
img
:
# Convert to RGB to handle PNG/RGBA correctly
if
img
.
mode
in
(
"RGBA"
,
"P"
):
img
=
img
.
convert
(
"RGB"
)
# Resize while maintaining aspect ratio, max 400px
img
.
thumbnail
((
400
,
400
))
# Save optimized JPEG
img
.
save
(
thumbnail_path
,
"JPEG"
,
quality
=
70
,
optimize
=
True
)
logger
.
info
(
f
"Saved optimized thumbnail to {thumbnail_path}"
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to generate thumbnail: {e}"
)
# Fallback: try to write original file if PIL fails, but rename extension if needed
try
:
with
open
(
thumbnail_path
,
"wb"
)
as
f
:
f
.
write
(
file_content
)
except
Exception
as
e2
:
logger
.
error
(
f
"Failed to save fallback thumbnail: {e2}"
)
thumbnail_url_path
=
None
files
=
{
'image'
:
(
image
.
filename
,
file_content
,
image
.
content_type
)
}
...
...
@@ -425,6 +472,13 @@ async def submit_video_job_proxy(
result
=
resp
.
json
()
# Inject thumbnail URL into the response so frontend can use it
if
thumbnail_url_path
:
logger
.
info
(
f
"Injecting thumbnail URL: {thumbnail_url_path}"
)
result
[
"thumbnail"
]
=
thumbnail_url_path
else
:
logger
.
warning
(
"Thumbnail URL path is None, skipping injection"
)
# 3. Increment Usage (Only if successful)
if
author_id
!=
ADMIN_ID
:
usage_store
.
increment_used
(
author_id
)
...
...
@@ -474,6 +528,10 @@ async def gallery_videos(limit: int = Query(200, ge=1, le=1000), author_id: Opti
@app.post
(
"/gallery/videos"
)
async
def
add_video
(
video
:
GalleryVideo
):
try
:
if
video
.
thumbnail
:
logger
.
info
(
f
"Saving video {video.id} with thumbnail: {video.thumbnail}"
)
else
:
logger
.
warning
(
f
"Saving video {video.id} WITHOUT thumbnail"
)
return
video_store
.
add_item
(
video
)
except
Exception
as
exc
:
raise
HTTPException
(
status_code
=
500
,
detail
=
f
"Failed to store video metadata: {exc}"
)
...
...
start_all.sh
View file @
efa4823
...
...
@@ -21,6 +21,8 @@ LOGS_DIR="$BASE_DIR/logs"
# Ensure logs directory exists
mkdir -p
"
$LOGS_DIR
"
# Ensure frontend dist exists or create it
mkdir -p
"
$FRONTEND_DIR
/dist"
echo
"=================================================="
echo
"Initializing Front-Backend Z-Image Services (PM2)"
...
...
@@ -40,7 +42,9 @@ window.APP_CONFIG = {
LIKES_FOR_REWARD: ${LIKES_FOR_REWARD:-5}
};
EOF
cp
"
$CONFIG_JS_FILE
"
"
$FRONTEND_DIR
/dist/config.js"
2>/dev/null
||
true
# Copy config to frontend dist to ensure it is served
cp
"
$CONFIG_JS_FILE
"
"
$FRONTEND_DIR
/dist/config.js"
export
TURBO_DIFFUSION_LOCAL_URL
=
"http://127.0.0.1:
$LOCAL_TURBO_PORT
"
export
VITE_API_BASE_URL
=
"http://
$PUBLIC_IP
:
$PUBLIC_BACKEND_PORT
"
export
WHITELIST_PATH
=
"
$BASE_DIR
/backend/whitelist.txt"
...
...
z-image-generator/App.tsx
View file @
efa4823
...
...
@@ -132,7 +132,7 @@ const App: React.FC = () => {
setVideoStatus({ status: 'submitting', message: '提交中...', task_id: 'temp' });
try {
const
taskId
= await submitVideoJob(params.prompt, imageFile, currentUser.employeeId, params.seed);
const
{ taskId, thumbnail }
= await submitVideoJob(params.prompt, imageFile, currentUser.employeeId, params.seed);
const finalStatus = await pollVideoStatus(taskId, setVideoStatus);
if (!finalStatus.video_filename) {
...
...
@@ -142,6 +142,7 @@ const App: React.FC = () => {
const newVideoData: ImageItem = {
id: `vid-${Date.now()}`,
url: `${VIDEO_OSS_BASE_URL}/${finalStatus.video_filename}`,
thumbnail: thumbnail, // Save the thumbnail URL
prompt: params.prompt,
authorId: currentUser.employeeId,
createdAt: Date.now(),
...
...
z-image-generator/components/ImageCard.tsx
View file @
efa4823
...
...
@@ -39,6 +39,10 @@ const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUs
}
}
// If it's a video with a thumbnail, we want to show it immediately (via poster)
// instead of waiting for the video file to load metadata.
const showContent = isLoaded || (isVideo && !!image.thumbnail);
return (
<div
className="group relative mb-4 break-inside-avoid rounded-2xl overflow-hidden bg-gray-200 cursor-zoom-in shadow-sm hover:shadow-xl transition-all duration-300"
...
...
@@ -47,7 +51,7 @@ const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUs
onMouseLeave={handleMouseLeave}
>
{/* Placeholder / Skeleton */}
{!
isLoaded
&& (
{!
showContent
&& (
<div className="absolute inset-0 bg-gray-200 animate-pulse min-h-[200px]" />
)}
...
...
@@ -56,7 +60,8 @@ const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUs
<video
ref={videoRef}
src={image.url}
className={`w-full h-auto object-cover transition-opacity duration-300 ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
poster={image.thumbnail} // Use thumbnail as poster
className={`w-full h-auto object-cover transition-opacity duration-300 ${showContent ? 'opacity-100' : 'opacity-0'}`}
onLoadedMetadata={handleVideoMetadata}
onLoadedData={() => setIsLoaded(true)}
loop
...
...
@@ -68,7 +73,7 @@ const ImageCard: React.FC<ImageCardProps> = ({ image, onClick, onLike, currentUs
<img
src={image.url}
alt={image.prompt}
className={`w-full h-auto object-cover transition-transform duration-700 ease-in-out group-hover:scale-105 ${
isLoaded
? 'opacity-100' : 'opacity-0'}`}
className={`w-full h-auto object-cover transition-transform duration-700 ease-in-out group-hover:scale-105 ${
showContent
? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setIsLoaded(true)}
loading="lazy"
/>
...
...
z-image-generator/services/videoService.ts
View file @
efa4823
...
...
@@ -51,9 +51,9 @@ const compressImage = async (file: File): Promise<File> => {
/**
* Submits a video generation job to the backend.
* @returns The task ID
for the submitted job
.
* @returns The task ID
and thumbnail URL
.
*/
export
const
submitVideoJob
=
async
(
prompt
:
string
,
image
:
File
,
authorId
:
string
,
seed
:
number
):
Promise
<
string
>
=>
{
export
const
submitVideoJob
=
async
(
prompt
:
string
,
image
:
File
,
authorId
:
string
,
seed
:
number
):
Promise
<
{
taskId
:
string
,
thumbnail
?:
string
}
>
=>
{
let
finalImage
=
image
;
if
(
image
.
size
>
1024
*
1024
)
{
finalImage
=
await
compressImage
(
image
);
...
...
@@ -85,8 +85,8 @@ export const submitVideoJob = async (prompt: string, image: File, authorId: stri
throw
new
Error
(
`
Job
submission
failed
:
$
{
errorText
}
`
);
}
const
{
task_id
}
=
await
submitRes
.
json
();
return
task_id
;
const
{
task_id
,
thumbnail
}
=
await
submitRes
.
json
();
return
{
taskId
:
task_id
,
thumbnail
};
};
/**
...
...
z-image-generator/types.ts
View file @
efa4823
...
...
@@ -13,6 +13,7 @@ export interface ImageGenerationParams {
export
interface
ImageItem
extends
ImageGenerationParams
{
id
:
string
;
url
:
string
;
// base64 data URI or http URL
thumbnail
?:
string
;
// Thumbnail URL for videos
createdAt
:
number
;
generationTime
?:
number
;
// Time in seconds for generation
...
...
Please
register
or
login
to post a comment