1591 lines
60 KiB
Python
1591 lines
60 KiB
Python
# Copyright 2026 Google LLC
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# https://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""GenMedia Live - Real-time multimodal AI creation application.
|
|
|
|
This application extends the Gemini Live API with image and video generation
|
|
capabilities using Gemini Pro Image and Veo through function calling.
|
|
"""
|
|
|
|
import asyncio
|
|
import base64
|
|
import logging
|
|
import os
|
|
import re
|
|
import threading
|
|
import traceback
|
|
from pathlib import Path
|
|
|
|
import google.auth
|
|
from flask import Flask, jsonify, request, send_from_directory
|
|
from flask_cors import CORS
|
|
from flask_socketio import SocketIO, emit
|
|
from google import genai
|
|
from google.cloud import storage
|
|
from google.genai import types
|
|
from google.oauth2.credentials import Credentials
|
|
|
|
try:
|
|
import io as pil_io
|
|
|
|
from PIL import Image
|
|
|
|
PIL_AVAILABLE = True
|
|
except ImportError:
|
|
PIL_AVAILABLE = False
|
|
|
|
app = Flask(__name__, static_folder="src", static_url_path="")
|
|
CORS(app)
|
|
logging.basicConfig(
|
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
|
)
|
|
|
|
socketio = SocketIO(
|
|
app,
|
|
cors_allowed_origins="*",
|
|
async_mode="threading",
|
|
ping_timeout=300,
|
|
ping_interval=60,
|
|
max_http_buffer_size=50000000,
|
|
transports=["polling"],
|
|
)
|
|
|
|
# Configuration
|
|
DEFAULT_PROJECT_ID = os.getenv("GOOGLE_CLOUD_PROJECT")
|
|
DEFAULT_LOCATION_ID = "us-central1"
|
|
|
|
if not DEFAULT_PROJECT_ID:
|
|
try:
|
|
_, project = google.auth.default()
|
|
if project:
|
|
DEFAULT_PROJECT_ID = project
|
|
except Exception:
|
|
pass
|
|
|
|
VEO_MODEL_ID = "veo-3.1-generate-001"
|
|
GEMINI_IMAGE_MODEL = "gemini-3-pro-image"
|
|
GEMINI_LIVE_MODEL = "gemini-live-2.5-flash-native-audio"
|
|
|
|
IMAGE_ASPECT_RATIOS = [
|
|
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"
|
|
]
|
|
VIDEO_ASPECT_RATIOS = ["16:9", "9:16"]
|
|
|
|
# Initialize clients
|
|
try:
|
|
default_gemini_client = genai.Client(
|
|
vertexai=True, project=DEFAULT_PROJECT_ID, location=DEFAULT_LOCATION_ID
|
|
)
|
|
default_image_client = genai.Client(
|
|
vertexai=True, project=DEFAULT_PROJECT_ID, location="global"
|
|
)
|
|
storage_client = storage.Client(project=DEFAULT_PROJECT_ID)
|
|
logging.info(f"Initialized with project: {DEFAULT_PROJECT_ID}")
|
|
except Exception as e:
|
|
logging.error(f"Initialization failed: {e}")
|
|
default_gemini_client = None
|
|
default_image_client = None
|
|
|
|
# Global state
|
|
session_credentials = {}
|
|
bridges = {}
|
|
live_sessions = {}
|
|
starting_sessions = set()
|
|
starting_session_sids = {}
|
|
session_states = {}
|
|
file_history = []
|
|
session_handles = {}
|
|
user_name = "User"
|
|
custom_system_instructions = ""
|
|
|
|
|
|
def sanitize_log(text, max_length=200):
|
|
"""Sanitize text for logging by removing base64 and image data."""
|
|
if not text:
|
|
return text
|
|
text = str(text)
|
|
text = re.sub(r"[A-Za-z0-9+/=]{50,}", "[base64]", text)
|
|
text = re.sub(r"data:image/[^;]+;base64,[^\s,)]+", "[img]", text)
|
|
return text[:max_length] + "..." if len(text) > max_length else text
|
|
|
|
|
|
def get_session_state(session_id):
|
|
"""Get or create session state for a given session ID."""
|
|
if session_id not in session_states:
|
|
session_states[session_id] = {
|
|
"last_image": None,
|
|
"last_video": None,
|
|
"last_seen_frame": None,
|
|
"uploaded_images": [],
|
|
}
|
|
return session_states[session_id]
|
|
|
|
|
|
def get_next_id_for_type(file_type, session_id):
|
|
"""Get the next available ID for a file type in a session."""
|
|
existing = [
|
|
e["id"]
|
|
for e in file_history
|
|
if e["type"] == file_type and e.get("session_id") == session_id
|
|
]
|
|
return 1 if not existing else max(existing) + 1
|
|
|
|
|
|
def get_last_id_for_type(file_type, session_id):
|
|
"""Get the last ID for a file type in a session."""
|
|
session_files = [
|
|
f
|
|
for f in file_history
|
|
if f.get("session_id") == session_id and f.get("type") == file_type
|
|
]
|
|
return max(f["id"] for f in session_files) if session_files else None
|
|
|
|
|
|
def get_active_client():
|
|
"""Get the active Gemini client using OAuth credentials."""
|
|
if "oauth" not in session_credentials:
|
|
return None
|
|
creds_data = session_credentials["oauth"]
|
|
creds = Credentials(token=creds_data["access_token"])
|
|
return genai.Client(
|
|
vertexai=True,
|
|
project=creds_data["project_id"],
|
|
location=creds_data["location"],
|
|
credentials=creds,
|
|
)
|
|
|
|
|
|
def get_active_image_client():
|
|
"""Get the active image generation client using OAuth credentials."""
|
|
if "oauth" not in session_credentials:
|
|
return None
|
|
creds_data = session_credentials["oauth"]
|
|
creds = Credentials(token=creds_data["access_token"])
|
|
return genai.Client(
|
|
vertexai=True,
|
|
project=creds_data["project_id"],
|
|
location="global",
|
|
credentials=creds,
|
|
)
|
|
|
|
|
|
def get_live_system_prompt(is_resumed=False):
|
|
"""Generate the system prompt for the Gemini Live API session."""
|
|
custom = f"\n{custom_system_instructions}\n" if custom_system_instructions else ""
|
|
name_section = (
|
|
f"\nUser's name: {user_name}\n" if user_name and user_name != "User" else ""
|
|
)
|
|
resume_note = (
|
|
"\nThis is a continued conversation. Continue naturally from where you left off.\n"
|
|
if is_resumed
|
|
else ""
|
|
)
|
|
|
|
return f"""You are GenMedia Live, a creative AI assistant with vision and media generation capabilities.
|
|
{custom}{name_section}{resume_note}
|
|
|
|
When the user asks you to SEE or DESCRIBE something (camera, uploaded image), just respond verbally - no tools needed.
|
|
When the user explicitly asks to GENERATE, CREATE, MAKE, or EDIT content, use the appropriate tool.
|
|
|
|
Available tools:
|
|
- generate_image: Create new images OR edit/modify existing ones by using them as reference
|
|
- generate_video: Create videos from text or animate images
|
|
- extract_frame: Get a frame from a video at a specific timestamp
|
|
- combine_videos: Merge multiple videos together
|
|
- view_generated_image: Load a previously generated image to see it
|
|
|
|
For image generation AND editing:
|
|
- To EDIT an existing image: set refers_to_last=true or use image_id to reference the image, then describe the changes in the prompt
|
|
- refers_to_camera=true: Use uploaded/camera image as reference
|
|
- refers_to_last=true: Use last generated image as reference for editing
|
|
- image_id: Reference a specific generated image by ID for editing
|
|
|
|
For video generation:
|
|
- Default duration is 8 seconds unless specified
|
|
- refers_to_last=true: Animate the last generated image
|
|
- image_id: Animate a specific image
|
|
|
|
Remember ALL conversation context and ALL previously generated content throughout the session.
|
|
Keep track of all generated images (image 1, image 2, etc.) and videos (video 1, video 2, etc.) by their IDs.
|
|
"""
|
|
|
|
|
|
class SessionBridge:
|
|
"""Bridge for communication between Flask and async Gemini Live session."""
|
|
|
|
def __init__(self, loop):
|
|
self.loop = loop
|
|
self.queue = asyncio.Queue(maxsize=100)
|
|
self.dropped_frames = 0
|
|
|
|
def put_nowait(self, item):
|
|
"""Put an item in the queue from a non-async context."""
|
|
if not self.loop.is_closed():
|
|
try:
|
|
self.loop.call_soon_threadsafe(self._safe_put, item)
|
|
except RuntimeError:
|
|
pass
|
|
|
|
def _safe_put(self, item):
|
|
"""Safely put an item in the queue, dropping old items if full."""
|
|
try:
|
|
self.queue.put_nowait(item)
|
|
except asyncio.QueueFull:
|
|
self.dropped_frames += 1
|
|
try:
|
|
self.queue.get_nowait()
|
|
self.queue.put_nowait(item)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
async def run_live_session(session_id, sid):
|
|
"""Run the Gemini Live API session."""
|
|
if session_id in live_sessions and live_sessions[session_id].get("active"):
|
|
live_sessions[session_id]["sid"] = sid
|
|
socketio.emit(
|
|
"live_session_started",
|
|
{"status": "reconnected", "user_name": user_name},
|
|
room=sid,
|
|
)
|
|
return
|
|
|
|
max_reconnects = 5
|
|
reconnect_count = 0
|
|
|
|
while reconnect_count < max_reconnects:
|
|
resumption_handle = session_handles.get(session_id)
|
|
is_resumed = resumption_handle is not None or reconnect_count > 0
|
|
if resumption_handle:
|
|
logging.info(f"Session {session_id}: resuming with handle")
|
|
input_queue = asyncio.Queue(maxsize=100)
|
|
|
|
loop = asyncio.get_event_loop()
|
|
bridge = SessionBridge(loop)
|
|
bridge.queue = input_queue
|
|
bridges[session_id] = bridge
|
|
|
|
try:
|
|
tools = [
|
|
types.Tool(
|
|
function_declarations=[
|
|
types.FunctionDeclaration(
|
|
name="generate_image",
|
|
description="Generate a new AI image OR edit/modify an existing image. For editing, use refers_to_last=true or image_id to reference the source image, and describe the desired changes in the prompt.",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"prompt": {
|
|
"type": "string",
|
|
"description": "Description of the image to generate",
|
|
},
|
|
"aspect_ratio": {
|
|
"type": "string",
|
|
"enum": IMAGE_ASPECT_RATIOS,
|
|
},
|
|
"refers_to_camera": {
|
|
"type": "boolean",
|
|
"description": "Use camera/uploaded image as reference",
|
|
},
|
|
"refers_to_last": {
|
|
"type": "boolean",
|
|
"description": "Use last generated image as reference",
|
|
},
|
|
"image_id": {
|
|
"type": "integer",
|
|
"description": "Specific image ID to reference",
|
|
},
|
|
},
|
|
"required": ["prompt"],
|
|
},
|
|
),
|
|
types.FunctionDeclaration(
|
|
name="generate_video",
|
|
description="Generate a video from text or animate an image.",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"prompt": {
|
|
"type": "string",
|
|
"description": "Description of video to generate",
|
|
},
|
|
"duration": {
|
|
"type": "integer",
|
|
"description": "Duration in seconds (4, 6, or 8)",
|
|
},
|
|
"refers_to_last": {
|
|
"type": "boolean",
|
|
"description": "Animate the last generated image",
|
|
},
|
|
"image_id": {
|
|
"type": "integer",
|
|
"description": "Specific image ID to animate",
|
|
},
|
|
},
|
|
"required": ["prompt"],
|
|
},
|
|
),
|
|
types.FunctionDeclaration(
|
|
name="extract_frame",
|
|
description="Extract a frame from a video at a specific timestamp",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"video_id": {
|
|
"type": "integer",
|
|
"description": "Video ID (defaults to last video)",
|
|
},
|
|
"timestamp": {
|
|
"type": "number",
|
|
"description": "Timestamp in seconds",
|
|
},
|
|
},
|
|
"required": [],
|
|
},
|
|
),
|
|
types.FunctionDeclaration(
|
|
name="combine_videos",
|
|
description="Combine multiple videos into one. Pass empty array to combine all session videos.",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"video_ids": {
|
|
"type": "array",
|
|
"items": {"type": "integer"},
|
|
"description": "Video IDs to combine",
|
|
},
|
|
},
|
|
"required": ["video_ids"],
|
|
},
|
|
),
|
|
types.FunctionDeclaration(
|
|
name="view_generated_image",
|
|
description="Load a previously generated image to view it",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"image_id": {
|
|
"type": "integer",
|
|
"description": "Image ID to view",
|
|
},
|
|
},
|
|
"required": ["image_id"],
|
|
},
|
|
),
|
|
]
|
|
)
|
|
]
|
|
|
|
config = types.LiveConnectConfig(
|
|
response_modalities=["AUDIO"],
|
|
system_instruction=get_live_system_prompt(is_resumed=is_resumed),
|
|
media_resolution=types.MediaResolution.MEDIA_RESOLUTION_MEDIUM,
|
|
context_window_compression=types.ContextWindowCompressionConfig(
|
|
trigger_tokens=100000,
|
|
sliding_window=types.SlidingWindow(target_tokens=80000),
|
|
),
|
|
session_resumption=types.SessionResumptionConfig(
|
|
handle=resumption_handle
|
|
),
|
|
speech_config=types.SpeechConfig(
|
|
voice_config=types.VoiceConfig(
|
|
prebuilt_voice_config=types.PrebuiltVoiceConfig(
|
|
voice_name="Aoede"
|
|
)
|
|
)
|
|
),
|
|
tools=tools,
|
|
)
|
|
|
|
client = get_active_client()
|
|
if client is None:
|
|
current_sid = starting_session_sids.get(session_id, sid)
|
|
socketio.emit(
|
|
"live_session_error",
|
|
{
|
|
"error": "Authentication required. Please enter your Project ID and access token.",
|
|
"code": 401,
|
|
},
|
|
room=current_sid,
|
|
)
|
|
starting_sessions.discard(session_id)
|
|
return
|
|
|
|
async with client.aio.live.connect(
|
|
model=GEMINI_LIVE_MODEL, config=config
|
|
) as session:
|
|
current_sid = starting_session_sids.get(session_id, sid)
|
|
live_sessions[session_id] = {"active": True, "sid": current_sid}
|
|
starting_sessions.discard(session_id)
|
|
starting_session_sids.pop(session_id, None)
|
|
socketio.emit(
|
|
"live_session_started",
|
|
{"status": "connected", "user_name": user_name},
|
|
room=current_sid,
|
|
)
|
|
|
|
async def sender_loop():
|
|
while live_sessions.get(session_id, {}).get("active"):
|
|
try:
|
|
item = await asyncio.wait_for(
|
|
input_queue.get(), timeout=0.5
|
|
)
|
|
|
|
if item["type"] == "audio":
|
|
await session.send_realtime_input(audio=item["data"])
|
|
elif item["type"] == "video":
|
|
await session.send_realtime_input(video=item["data"])
|
|
elif item["type"] == "text":
|
|
await session.send_client_content(
|
|
turns=types.Content(
|
|
role="user",
|
|
parts=[types.Part(text=item["data"])],
|
|
),
|
|
turn_complete=True,
|
|
)
|
|
elif item["type"] == "image_with_text":
|
|
for img_data in item.get("images", []):
|
|
if isinstance(img_data, dict):
|
|
blob = types.Blob(
|
|
mime_type=img_data.get(
|
|
"mime_type", "image/jpeg"
|
|
),
|
|
data=img_data["data"],
|
|
)
|
|
for _ in range(5):
|
|
await session.send_realtime_input(
|
|
video=blob
|
|
)
|
|
await asyncio.sleep(0.1)
|
|
await session.send_client_content(
|
|
turns=types.Content(
|
|
role="user",
|
|
parts=[
|
|
types.Part(text=item.get("text", ""))
|
|
],
|
|
),
|
|
turn_complete=True,
|
|
)
|
|
elif item["type"] == "image":
|
|
for img_data in item["data"]:
|
|
if isinstance(img_data, dict):
|
|
blob = types.Blob(
|
|
mime_type=img_data.get(
|
|
"mime_type", "image/jpeg"
|
|
),
|
|
data=img_data["data"],
|
|
)
|
|
for _ in range(3):
|
|
await session.send_realtime_input(
|
|
video=blob
|
|
)
|
|
await asyncio.sleep(0.1)
|
|
elif item["type"] == "tool_response":
|
|
await session.send_tool_response(
|
|
function_responses=item["data"]
|
|
)
|
|
elif item["type"] == "generated_image_feedback":
|
|
img_data = item["data"]
|
|
blob = types.Blob(
|
|
mime_type=img_data.get("mime_type", "image/png"),
|
|
data=img_data["data"],
|
|
)
|
|
await session.send_client_content(
|
|
turns=types.Content(
|
|
role="user",
|
|
parts=[
|
|
types.Part(inline_data=blob),
|
|
types.Part(
|
|
text=f"[Generated image {item.get('id', '?')}]"
|
|
),
|
|
],
|
|
),
|
|
turn_complete=False,
|
|
)
|
|
input_queue.task_done()
|
|
except asyncio.TimeoutError:
|
|
continue
|
|
except Exception as e:
|
|
logging.error(f"Send error: {e}")
|
|
|
|
async def receiver_loop():
|
|
try:
|
|
while live_sessions.get(session_id, {}).get("active"):
|
|
async for response in session.receive():
|
|
if not live_sessions.get(session_id, {}).get("active"):
|
|
return "ended"
|
|
current_sid = live_sessions[session_id]["sid"]
|
|
|
|
if response.session_resumption_update:
|
|
update = response.session_resumption_update
|
|
if update.resumable and update.new_handle:
|
|
session_handles[session_id] = update.new_handle
|
|
logging.info(
|
|
f"Session {session_id}: captured resumption handle"
|
|
)
|
|
|
|
if response.tool_call:
|
|
for fc in response.tool_call.function_calls:
|
|
socketio.emit(
|
|
"voice_generation_request",
|
|
{
|
|
"text": "Generating...",
|
|
"function_name": fc.name,
|
|
"function_args": dict(fc.args),
|
|
"function_call_id": fc.id,
|
|
},
|
|
room=current_sid,
|
|
)
|
|
|
|
if (
|
|
response.server_content
|
|
and response.server_content.model_turn
|
|
):
|
|
for (
|
|
part
|
|
) in response.server_content.model_turn.parts:
|
|
if part.text:
|
|
socketio.emit(
|
|
"text_response",
|
|
{"text": part.text},
|
|
room=current_sid,
|
|
)
|
|
if part.inline_data:
|
|
audio_b64 = base64.b64encode(
|
|
part.inline_data.data
|
|
).decode("utf-8")
|
|
socketio.emit(
|
|
"audio_response",
|
|
{
|
|
"audio": audio_b64,
|
|
"mime_type": part.inline_data.mime_type,
|
|
},
|
|
room=current_sid,
|
|
)
|
|
except asyncio.CancelledError:
|
|
return "cancelled"
|
|
except Exception as e:
|
|
error_msg = str(e)
|
|
if (
|
|
"1011" in error_msg
|
|
or "Insufficient model resources" in error_msg
|
|
):
|
|
current_sid = live_sessions[session_id]["sid"]
|
|
socketio.emit(
|
|
"live_session_error",
|
|
{
|
|
"error": "Server overloaded. Please try again.",
|
|
"code": 1011,
|
|
},
|
|
room=current_sid,
|
|
)
|
|
return "capacity_error"
|
|
if "1000" in error_msg or "cancelled" in error_msg.lower():
|
|
return "reconnect"
|
|
logging.error(f"Receive error: {e}")
|
|
return "error"
|
|
return "ended"
|
|
|
|
sender_task = asyncio.create_task(sender_loop())
|
|
receiver_task = asyncio.create_task(receiver_loop())
|
|
|
|
done, pending = await asyncio.wait(
|
|
[sender_task, receiver_task], return_when=asyncio.FIRST_COMPLETED
|
|
)
|
|
|
|
session_active = live_sessions.get(session_id, {}).get("active", False)
|
|
should_reconnect = False
|
|
for task in done:
|
|
try:
|
|
result = task.result()
|
|
if result == "reconnect":
|
|
should_reconnect = True
|
|
elif result == "ended" and session_active:
|
|
should_reconnect = True
|
|
except Exception:
|
|
pass
|
|
|
|
for task in pending:
|
|
task.cancel()
|
|
try:
|
|
await task
|
|
except asyncio.CancelledError:
|
|
pass
|
|
|
|
session_state = get_session_state(session_id)
|
|
session_state["should_reconnect"] = should_reconnect
|
|
|
|
session_active = live_sessions.get(session_id, {}).get("active", False)
|
|
should_reconnect = get_session_state(session_id).get(
|
|
"should_reconnect", False
|
|
)
|
|
has_handle = session_id in session_handles
|
|
|
|
if session_active and (should_reconnect or has_handle):
|
|
reconnect_count += 1
|
|
logging.info(
|
|
f"Reconnecting session {session_id} (attempt {reconnect_count}, has_handle={has_handle})"
|
|
)
|
|
await asyncio.sleep(1)
|
|
continue
|
|
else:
|
|
break
|
|
|
|
except Exception as e:
|
|
logging.error(f"Session error: {e}")
|
|
current_sid = live_sessions.get(session_id, {}).get("sid", sid)
|
|
error_msg = str(e)
|
|
|
|
if "1011" in error_msg or "Insufficient model resources" in error_msg:
|
|
socketio.emit(
|
|
"live_session_error",
|
|
{
|
|
"error": "Server overloaded. Please try again.",
|
|
"code": 1011,
|
|
},
|
|
room=current_sid,
|
|
)
|
|
break
|
|
|
|
socketio.emit(
|
|
"live_session_error", {"error": str(e)}, room=current_sid
|
|
)
|
|
|
|
if (
|
|
"1000" in error_msg or "cancelled" in error_msg.lower()
|
|
) and reconnect_count < max_reconnects:
|
|
reconnect_count += 1
|
|
await asyncio.sleep(2)
|
|
continue
|
|
else:
|
|
break
|
|
|
|
if session_id in bridges:
|
|
del bridges[session_id]
|
|
if session_id in live_sessions:
|
|
final_sid = live_sessions[session_id]["sid"]
|
|
del live_sessions[session_id]
|
|
socketio.emit(
|
|
"session_ended_reconnect",
|
|
{"session_id": session_id, "can_resume": session_id in session_handles},
|
|
room=final_sid,
|
|
)
|
|
starting_sessions.discard(session_id)
|
|
|
|
|
|
def start_background_loop(session_id, sid):
|
|
"""Start the async event loop for the Gemini Live session."""
|
|
loop = asyncio.new_event_loop()
|
|
asyncio.set_event_loop(loop)
|
|
try:
|
|
loop.run_until_complete(run_live_session(session_id, sid))
|
|
except Exception as e:
|
|
logging.error(f"Loop error: {e}")
|
|
finally:
|
|
starting_sessions.discard(session_id)
|
|
loop.close()
|
|
|
|
|
|
# Socket handlers
|
|
@socketio.on("connect")
|
|
def handle_connect():
|
|
"""Handle client connection."""
|
|
logging.info(f"Client connected: {request.sid}")
|
|
|
|
|
|
@socketio.on("disconnect")
|
|
def handle_disconnect():
|
|
"""Handle client disconnection."""
|
|
logging.info(f"Client disconnected: {request.sid}")
|
|
|
|
|
|
@socketio.on("start_live_session")
|
|
def handle_start(data):
|
|
"""Handle start live session request."""
|
|
session_id = data.get("session_id", "default")
|
|
sid = request.sid
|
|
|
|
session_state = get_session_state(session_id)
|
|
session_state["uploaded_images"] = []
|
|
session_state["last_seen_frame"] = None
|
|
|
|
if session_id in bridges and session_id in live_sessions:
|
|
live_sessions[session_id]["sid"] = sid
|
|
emit("live_session_started", {"status": "reconnected", "user_name": user_name})
|
|
return
|
|
|
|
if session_id in starting_sessions:
|
|
starting_session_sids[session_id] = sid
|
|
return
|
|
|
|
starting_sessions.add(session_id)
|
|
starting_session_sids[session_id] = sid
|
|
t = threading.Thread(
|
|
target=start_background_loop, args=(session_id, sid), daemon=True
|
|
)
|
|
t.start()
|
|
|
|
|
|
@socketio.on("stop_live_session")
|
|
def handle_stop(data):
|
|
"""Handle stop live session request."""
|
|
session_id = data.get("session_id")
|
|
if session_id in live_sessions:
|
|
live_sessions[session_id]["active"] = False
|
|
emit("live_session_stopped")
|
|
|
|
|
|
@socketio.on("check_session_status")
|
|
def handle_check_session(data):
|
|
"""Check if a session is active."""
|
|
session_id = data.get("session_id")
|
|
sid = request.sid
|
|
|
|
if session_id in bridges and session_id in live_sessions:
|
|
live_sessions[session_id]["sid"] = sid
|
|
emit("live_session_started", {"status": "reconnected", "user_name": user_name})
|
|
return {"active": True}
|
|
elif session_id in starting_sessions:
|
|
starting_session_sids[session_id] = sid
|
|
return {"active": False, "starting": True}
|
|
return {"active": False}
|
|
|
|
|
|
@socketio.on("send_audio")
|
|
def handle_audio(data):
|
|
"""Handle incoming audio data."""
|
|
session_id = data.get("session_id")
|
|
audio = data.get("audio")
|
|
|
|
if not session_id or session_id not in bridges or not audio:
|
|
return
|
|
|
|
try:
|
|
import struct
|
|
|
|
if isinstance(audio, list):
|
|
b = struct.pack(f"<{len(audio)}h", *audio)
|
|
else:
|
|
b = base64.b64decode(audio)
|
|
audio_blob = types.Blob(mime_type="audio/pcm;rate=16000", data=b)
|
|
bridges[session_id].put_nowait({"type": "audio", "data": audio_blob})
|
|
except Exception as e:
|
|
logging.error(f"Audio error: {e}")
|
|
|
|
|
|
@socketio.on("send_camera_frame")
|
|
def handle_video(data):
|
|
"""Handle incoming camera frame."""
|
|
session_id = data.get("session_id")
|
|
frame = data.get("frame")
|
|
|
|
if session_id and frame:
|
|
get_session_state(session_id)[
|
|
"last_seen_frame"
|
|
] = f"data:image/jpeg;base64,{frame}"
|
|
|
|
if session_id in bridges and frame:
|
|
try:
|
|
frame_bytes = base64.b64decode(frame)
|
|
video_blob = types.Blob(mime_type="image/jpeg", data=frame_bytes)
|
|
bridges[session_id].put_nowait({"type": "video", "data": video_blob})
|
|
except Exception as e:
|
|
logging.error(f"Frame error: {e}")
|
|
|
|
|
|
@socketio.on("send_text_message")
|
|
def handle_text(data):
|
|
"""Handle incoming text message."""
|
|
session_id = data.get("session_id")
|
|
text = data.get("text")
|
|
if session_id in bridges and text:
|
|
bridges[session_id].put_nowait({"type": "text", "data": text})
|
|
|
|
|
|
@socketio.on("send_uploaded_images")
|
|
def handle_uploaded_images(data):
|
|
"""Handle uploaded images."""
|
|
session_id = data.get("session_id")
|
|
images = data.get("images", [])
|
|
|
|
if not session_id or not images:
|
|
return {"status": "error", "message": "No session_id or images"}
|
|
|
|
session_state = get_session_state(session_id)
|
|
session_state["uploaded_images"] = images
|
|
if images:
|
|
session_state["last_seen_frame"] = images[-1]
|
|
|
|
if session_id in bridges and session_id in live_sessions:
|
|
try:
|
|
processed_images = []
|
|
for img_data_url in images:
|
|
if isinstance(img_data_url, str) and "," in img_data_url:
|
|
header, b64_data = img_data_url.split(",", 1)
|
|
img_bytes = base64.b64decode(b64_data)
|
|
|
|
if PIL_AVAILABLE:
|
|
img = Image.open(pil_io.BytesIO(img_bytes))
|
|
if img.mode in ("RGBA", "P"):
|
|
img = img.convert("RGB")
|
|
img.thumbnail((768, 768), Image.Resampling.LANCZOS)
|
|
canvas = Image.new("RGB", (768, 768), (128, 128, 128))
|
|
x = (768 - img.width) // 2
|
|
y = (768 - img.height) // 2
|
|
canvas.paste(img, (x, y))
|
|
buffer = pil_io.BytesIO()
|
|
canvas.save(buffer, format="JPEG", quality=85)
|
|
processed_images.append(
|
|
{"mime_type": "image/jpeg", "data": buffer.getvalue()}
|
|
)
|
|
else:
|
|
mime_type = "image/jpeg" if "jpeg" in header else "image/png"
|
|
processed_images.append(
|
|
{"mime_type": mime_type, "data": img_bytes}
|
|
)
|
|
|
|
if processed_images:
|
|
bridges[session_id].put_nowait(
|
|
{"type": "image", "data": processed_images}
|
|
)
|
|
return {"status": "ok", "queued": len(processed_images)}
|
|
except Exception as e:
|
|
logging.error(f"Image upload error: {e}")
|
|
return {"status": "error", "message": str(e)}
|
|
return {"status": "stored"}
|
|
|
|
|
|
@socketio.on("send_message_with_images")
|
|
def handle_message_with_images(data):
|
|
"""Handle text message with images."""
|
|
session_id = data.get("session_id")
|
|
text = data.get("text", "")
|
|
images = data.get("images", [])
|
|
|
|
if not session_id or session_id not in bridges:
|
|
return {"status": "error", "message": "Session not active"}
|
|
|
|
session_state = get_session_state(session_id)
|
|
session_state["uploaded_images"] = images
|
|
if images:
|
|
session_state["last_seen_frame"] = images[-1]
|
|
|
|
try:
|
|
processed_images = []
|
|
for img_data_url in images:
|
|
if isinstance(img_data_url, str) and "," in img_data_url:
|
|
header, b64_data = img_data_url.split(",", 1)
|
|
img_bytes = base64.b64decode(b64_data)
|
|
mime_type = "image/jpeg"
|
|
if "image/png" in header:
|
|
mime_type = "image/png"
|
|
|
|
if PIL_AVAILABLE:
|
|
img = Image.open(pil_io.BytesIO(img_bytes))
|
|
if img.mode in ("RGBA", "P"):
|
|
img = img.convert("RGB")
|
|
img.thumbnail((768, 768), Image.Resampling.LANCZOS)
|
|
canvas = Image.new("RGB", (768, 768), (128, 128, 128))
|
|
x = (768 - img.width) // 2
|
|
y = (768 - img.height) // 2
|
|
canvas.paste(img, (x, y))
|
|
buffer = pil_io.BytesIO()
|
|
canvas.save(buffer, format="JPEG", quality=85)
|
|
processed_images.append(
|
|
{"mime_type": "image/jpeg", "data": buffer.getvalue()}
|
|
)
|
|
else:
|
|
processed_images.append({"mime_type": mime_type, "data": img_bytes})
|
|
|
|
context = f"[User uploaded {len(images)} image(s)]: {text}" if images else text
|
|
bridges[session_id].put_nowait(
|
|
{"type": "image_with_text", "images": processed_images, "text": context}
|
|
)
|
|
return {"status": "ok"}
|
|
except Exception as e:
|
|
logging.error(f"Message with images error: {e}")
|
|
return {"status": "error", "message": str(e)}
|
|
|
|
|
|
@socketio.on("generation_completed")
|
|
def handle_gen_complete(data):
|
|
"""Handle generation completed notification."""
|
|
session_id = data.get("session_id")
|
|
gen_id = data.get("id")
|
|
gen_type = data.get("type")
|
|
call_id = data.get("function_call_id")
|
|
|
|
if session_id in bridges and call_id:
|
|
response = [
|
|
types.FunctionResponse(
|
|
id=call_id,
|
|
name="generate_content",
|
|
response={"status": "completed", "result": f"Generated {gen_type} {gen_id}"},
|
|
)
|
|
]
|
|
bridges[session_id].put_nowait({"type": "tool_response", "data": response})
|
|
|
|
if gen_type == "image":
|
|
try:
|
|
img_path = Path(f"outputs/images/image_{gen_id}.png")
|
|
if img_path.exists():
|
|
img_bytes = img_path.read_bytes()
|
|
bridges[session_id].put_nowait(
|
|
{
|
|
"type": "generated_image_feedback",
|
|
"data": {"mime_type": "image/png", "data": img_bytes},
|
|
"id": gen_id,
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Error sending generated image: {e}")
|
|
|
|
|
|
# REST API routes
|
|
@app.route("/api/genmedia-chat", methods=["POST"])
|
|
def handle_genmedia_chat():
|
|
"""Handle generation requests from the frontend."""
|
|
try:
|
|
data = request.json
|
|
if not data:
|
|
return jsonify({"error": "No data received"}), 400
|
|
|
|
session_id = data.get("session_id", "default")
|
|
|
|
tool_name = data.get("function_name") or data.get("tool") or data.get("name")
|
|
tool_decision = data.get("tool_decision") or {}
|
|
if isinstance(tool_decision, dict):
|
|
tool_name = tool_name or tool_decision.get(
|
|
"function_name"
|
|
) or tool_decision.get("tool")
|
|
|
|
args = {}
|
|
if data.get("function_args") and isinstance(data.get("function_args"), dict):
|
|
args = data.get("function_args")
|
|
elif tool_decision.get("function_args") and isinstance(
|
|
tool_decision.get("function_args"), dict
|
|
):
|
|
args = tool_decision.get("function_args")
|
|
|
|
if not tool_name:
|
|
return jsonify({"error": "No tool name found"}), 400
|
|
|
|
if tool_name in ["nano_banana", "generate_image"]:
|
|
return handle_image_generation(args, session_id)
|
|
elif tool_name in ["veo", "generate_video"]:
|
|
return handle_video_generation(args, session_id)
|
|
elif tool_name == "extract_frame":
|
|
video_id = args.get("video_id") or get_last_id_for_type("video", session_id)
|
|
if video_id is None:
|
|
return jsonify({"error": "No videos found"}), 404
|
|
return handle_extract_frame(
|
|
{"video_id": video_id, "timestamp": args.get("timestamp", 0)},
|
|
session_id,
|
|
)
|
|
elif tool_name == "combine_videos":
|
|
video_ids = args.get("video_ids", [])
|
|
if len(video_ids) < 2:
|
|
session_videos = [
|
|
f
|
|
for f in file_history
|
|
if f.get("session_id") == session_id and f.get("type") == "video"
|
|
]
|
|
video_ids = sorted([f["id"] for f in session_videos])
|
|
if len(video_ids) < 2:
|
|
return jsonify({"error": "Need at least 2 videos"}), 400
|
|
return handle_combine_videos({"video_ids": video_ids}, session_id)
|
|
elif tool_name == "view_generated_image":
|
|
image_id = args.get("image_id")
|
|
if image_id is None:
|
|
return jsonify({"error": "No image_id provided"}), 400
|
|
img_path = Path(f"outputs/images/image_{image_id}.png")
|
|
if not img_path.exists():
|
|
return jsonify({"error": f"Image {image_id} not found"}), 404
|
|
if session_id in bridges:
|
|
bridges[session_id].put_nowait(
|
|
{
|
|
"type": "generated_image_feedback",
|
|
"data": {"mime_type": "image/png", "data": img_path.read_bytes()},
|
|
"id": image_id,
|
|
}
|
|
)
|
|
return jsonify({"response": f"Image {image_id} loaded", "image_id": image_id})
|
|
|
|
return jsonify({"response": "Unknown tool", "tool": tool_name})
|
|
except Exception as e:
|
|
logging.error(f"API error: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
def handle_image_generation(args, session_id):
|
|
"""Handle image generation requests."""
|
|
session_state = get_session_state(session_id)
|
|
prompt = args.get("prompt")
|
|
aspect_ratio = args.get("aspect_ratio", args.get("aspectRatio", "1:1"))
|
|
if aspect_ratio not in IMAGE_ASPECT_RATIOS:
|
|
aspect_ratio = "1:1"
|
|
|
|
ref_images = args.get("reference_images", [])
|
|
refers_to_camera = args.get("refers_to_camera", False)
|
|
refers_to_last = args.get("refers_to_last", False)
|
|
specific_image_id = args.get("image_id")
|
|
if specific_image_id is not None:
|
|
specific_image_id = int(specific_image_id)
|
|
|
|
if refers_to_camera:
|
|
if session_state.get("uploaded_images"):
|
|
ref_images.extend(session_state.get("uploaded_images"))
|
|
elif session_state.get("last_seen_frame"):
|
|
ref_images.append(session_state.get("last_seen_frame"))
|
|
|
|
if specific_image_id:
|
|
session_images = [
|
|
f
|
|
for f in file_history
|
|
if f.get("session_id") == session_id and f.get("type") == "image"
|
|
]
|
|
matching = [f for f in session_images if f.get("id") == specific_image_id]
|
|
if matching:
|
|
img_path = Path(matching[0]["path"])
|
|
if img_path.exists():
|
|
with open(img_path, "rb") as f:
|
|
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
ref_images.append(f"data:image/png;base64,{img_data}")
|
|
|
|
if refers_to_last and not specific_image_id:
|
|
last_id = get_last_id_for_type("image", session_id)
|
|
if last_id:
|
|
img_path = Path(f"outputs/images/image_{last_id}.png")
|
|
if img_path.exists():
|
|
with open(img_path, "rb") as f:
|
|
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
ref_images.append(f"data:image/png;base64,{img_data}")
|
|
|
|
try:
|
|
generation_config = types.GenerateContentConfig(
|
|
temperature=0.7,
|
|
response_modalities=["TEXT", "IMAGE"],
|
|
image_config=types.ImageConfig(aspect_ratio=aspect_ratio, image_size="2K"),
|
|
)
|
|
|
|
content_parts = []
|
|
for ref_img in ref_images[:4]:
|
|
if isinstance(ref_img, str) and ref_img.startswith("data:"):
|
|
try:
|
|
b = base64.b64decode(ref_img.split(",")[1])
|
|
content_parts.append(
|
|
{"inline_data": {"mime_type": "image/png", "data": b}}
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
if content_parts:
|
|
content_parts.append(f"Based on the reference image(s), create: {prompt}")
|
|
else:
|
|
content_parts.append(prompt)
|
|
|
|
client = get_active_image_client()
|
|
if client is None:
|
|
return (
|
|
jsonify(
|
|
{
|
|
"error": "Authentication required. Please enter your Project ID and access token."
|
|
}
|
|
),
|
|
401,
|
|
)
|
|
response = client.models.generate_content(
|
|
model=GEMINI_IMAGE_MODEL, contents=content_parts, config=generation_config
|
|
)
|
|
|
|
output_dir = Path("outputs/images")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
next_id = get_next_id_for_type("image", session_id)
|
|
image_path = output_dir / f"image_{next_id}.png"
|
|
|
|
image_saved = False
|
|
for candidate in response.candidates:
|
|
if candidate.content and candidate.content.parts:
|
|
for part in candidate.content.parts:
|
|
if part.inline_data:
|
|
image_path.write_bytes(part.inline_data.data)
|
|
image_saved = True
|
|
break
|
|
|
|
if not image_saved:
|
|
return jsonify({"error": "No image generated"}), 500
|
|
|
|
session_state["last_image"] = str(image_path)
|
|
file_history.append(
|
|
{"id": next_id, "type": "image", "path": str(image_path), "session_id": session_id}
|
|
)
|
|
|
|
return jsonify(
|
|
{
|
|
"response": f"Generated image {next_id}",
|
|
"file": {
|
|
"id": next_id,
|
|
"type": "image",
|
|
"url": f"/outputs/images/image_{next_id}.png",
|
|
},
|
|
}
|
|
)
|
|
except Exception as e:
|
|
logging.error(f"Image generation error: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
def handle_video_generation(args, session_id):
|
|
"""Handle video generation requests."""
|
|
import time
|
|
|
|
session_state = get_session_state(session_id)
|
|
prompt = args.get("prompt", "")
|
|
duration = args.get("duration", 8)
|
|
refers_to_last = args.get("refers_to_last", False)
|
|
image_id = args.get("image_id")
|
|
if image_id is not None:
|
|
image_id = int(image_id)
|
|
|
|
if duration <= 4:
|
|
duration = 4
|
|
elif duration <= 6:
|
|
duration = 6
|
|
else:
|
|
duration = 8
|
|
|
|
try:
|
|
reference_image = None
|
|
|
|
if image_id:
|
|
img_path = Path(f"outputs/images/image_{image_id}.png")
|
|
if img_path.exists():
|
|
reference_image = img_path
|
|
elif refers_to_last:
|
|
last_id = get_last_id_for_type("image", session_id)
|
|
if last_id:
|
|
img_path = Path(f"outputs/images/image_{last_id}.png")
|
|
if img_path.exists():
|
|
reference_image = img_path
|
|
|
|
if "oauth" not in session_credentials:
|
|
return (
|
|
jsonify(
|
|
{
|
|
"error": "Authentication required. Please enter your Project ID and access token."
|
|
}
|
|
),
|
|
401,
|
|
)
|
|
creds_data = session_credentials["oauth"]
|
|
creds = Credentials(token=creds_data["access_token"])
|
|
video_client = genai.Client(
|
|
vertexai=True,
|
|
project=creds_data["project_id"],
|
|
location="global",
|
|
credentials=creds,
|
|
)
|
|
|
|
output_dir = Path("outputs/videos")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
next_id = get_next_id_for_type("video", session_id)
|
|
video_path = output_dir / f"video_{next_id}.mp4"
|
|
|
|
video_config = types.GenerateVideosConfig(
|
|
aspect_ratio="16:9",
|
|
number_of_videos=1,
|
|
duration_seconds=duration,
|
|
enhance_prompt=True,
|
|
person_generation="allow_adults",
|
|
)
|
|
|
|
if reference_image and reference_image.exists():
|
|
operation = video_client.models.generate_videos(
|
|
model=VEO_MODEL_ID,
|
|
prompt=prompt,
|
|
image=types.Image(
|
|
image_bytes=reference_image.read_bytes(), mime_type="image/png"
|
|
),
|
|
config=video_config,
|
|
)
|
|
else:
|
|
operation = video_client.models.generate_videos(
|
|
model=VEO_MODEL_ID,
|
|
prompt=prompt,
|
|
config=video_config,
|
|
)
|
|
|
|
max_wait = 300
|
|
waited = 0
|
|
while not operation.done and waited < max_wait:
|
|
time.sleep(15)
|
|
waited += 15
|
|
operation = video_client.operations.get(operation)
|
|
|
|
if not operation.done:
|
|
return jsonify({"error": "Video generation timed out"}), 500
|
|
|
|
result = operation.result if hasattr(operation, "result") else operation.response
|
|
if not result or not result.generated_videos:
|
|
return jsonify({"error": "No video generated"}), 500
|
|
|
|
video = result.generated_videos[0].video
|
|
|
|
if hasattr(video, "video_bytes") and video.video_bytes:
|
|
video_path.write_bytes(video.video_bytes)
|
|
elif hasattr(video, "uri") and video.uri:
|
|
gcs_match = re.match(r"gs://([^/]+)/(.+)", video.uri)
|
|
if gcs_match:
|
|
bucket_name, blob_name = gcs_match.groups()
|
|
bucket = storage_client.bucket(bucket_name)
|
|
blob = bucket.blob(blob_name)
|
|
blob.download_to_filename(str(video_path))
|
|
else:
|
|
return jsonify({"error": "Invalid video URI"}), 500
|
|
else:
|
|
return jsonify({"error": "No video data available"}), 500
|
|
|
|
session_state["last_video"] = str(video_path)
|
|
file_history.append(
|
|
{"id": next_id, "type": "video", "path": str(video_path), "session_id": session_id}
|
|
)
|
|
|
|
return jsonify(
|
|
{
|
|
"response": f"Generated video {next_id}",
|
|
"file": {
|
|
"id": next_id,
|
|
"type": "video",
|
|
"url": f"/outputs/videos/video_{next_id}.mp4",
|
|
},
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Video generation error: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
def handle_extract_frame(args, session_id):
|
|
"""Handle frame extraction from video."""
|
|
import subprocess
|
|
|
|
video_id = args.get("video_id", 1)
|
|
if video_id is not None:
|
|
video_id = int(video_id)
|
|
timestamp = args.get("timestamp", 0)
|
|
|
|
try:
|
|
video_path = Path(f"outputs/videos/video_{video_id}.mp4")
|
|
if not video_path.exists():
|
|
for f in file_history:
|
|
if f.get("type") == "video" and f.get("id") == video_id:
|
|
video_path = Path(f["path"])
|
|
break
|
|
|
|
if not video_path.exists():
|
|
return jsonify({"error": f"Video {video_id} not found"}), 404
|
|
|
|
output_dir = Path("outputs/images")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
next_id = get_next_id_for_type("image", session_id)
|
|
frame_path = output_dir / f"image_{next_id}.png"
|
|
|
|
cmd = [
|
|
"ffmpeg",
|
|
"-y",
|
|
"-ss",
|
|
str(timestamp),
|
|
"-i",
|
|
str(video_path),
|
|
"-vframes",
|
|
"1",
|
|
"-f",
|
|
"image2",
|
|
"-vcodec",
|
|
"png",
|
|
str(frame_path),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode != 0 or not frame_path.exists():
|
|
return jsonify({"error": "Frame extraction failed"}), 500
|
|
|
|
session_state = get_session_state(session_id)
|
|
session_state["last_image"] = str(frame_path)
|
|
file_history.append(
|
|
{"id": next_id, "type": "image", "path": str(frame_path), "session_id": session_id}
|
|
)
|
|
|
|
return jsonify(
|
|
{
|
|
"response": f"Extracted frame {next_id} from video {video_id}",
|
|
"file": {
|
|
"id": next_id,
|
|
"type": "image",
|
|
"url": f"/outputs/images/image_{next_id}.png",
|
|
},
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Frame extraction error: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
def handle_combine_videos(args, session_id):
|
|
"""Handle combining multiple videos."""
|
|
import subprocess
|
|
import tempfile
|
|
|
|
video_ids = args.get("video_ids", [])
|
|
video_ids = [int(v) for v in video_ids]
|
|
|
|
if len(video_ids) < 2:
|
|
return jsonify({"error": "Need at least 2 videos"}), 400
|
|
|
|
try:
|
|
video_paths = []
|
|
for vid_id in video_ids:
|
|
video_path = Path(f"outputs/videos/video_{vid_id}.mp4")
|
|
if not video_path.exists():
|
|
for f in file_history:
|
|
if f.get("type") == "video" and f.get("id") == vid_id:
|
|
video_path = Path(f["path"])
|
|
break
|
|
if not video_path.exists():
|
|
return jsonify({"error": f"Video {vid_id} not found"}), 404
|
|
video_paths.append(video_path)
|
|
|
|
output_dir = Path("outputs/videos")
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
next_id = get_next_id_for_type("video", session_id)
|
|
output_path = output_dir / f"video_{next_id}.mp4"
|
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
|
concat_file = f.name
|
|
for vp in video_paths:
|
|
f.write(f"file '{vp.absolute()}'\n")
|
|
|
|
cmd = [
|
|
"ffmpeg",
|
|
"-y",
|
|
"-f",
|
|
"concat",
|
|
"-safe",
|
|
"0",
|
|
"-i",
|
|
concat_file,
|
|
"-c",
|
|
"copy",
|
|
str(output_path),
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
Path(concat_file).unlink(missing_ok=True)
|
|
|
|
if result.returncode != 0:
|
|
cmd = [
|
|
"ffmpeg",
|
|
"-y",
|
|
"-f",
|
|
"concat",
|
|
"-safe",
|
|
"0",
|
|
"-i",
|
|
concat_file,
|
|
"-c:v",
|
|
"libx264",
|
|
"-c:a",
|
|
"aac",
|
|
"-preset",
|
|
"fast",
|
|
str(output_path),
|
|
]
|
|
subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if not output_path.exists():
|
|
return jsonify({"error": "Video combination failed"}), 500
|
|
|
|
session_state = get_session_state(session_id)
|
|
session_state["last_video"] = str(output_path)
|
|
file_history.append(
|
|
{"id": next_id, "type": "video", "path": str(output_path), "session_id": session_id}
|
|
)
|
|
|
|
return jsonify(
|
|
{
|
|
"response": f"Combined {len(video_ids)} videos into video {next_id}",
|
|
"file": {
|
|
"id": next_id,
|
|
"type": "video",
|
|
"url": f"/outputs/videos/video_{next_id}.mp4",
|
|
},
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Video combination error: {e}")
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
@app.route("/api/validate-token", methods=["POST"])
|
|
def validate_token():
|
|
"""Validate user's access token."""
|
|
global session_credentials
|
|
data = request.json
|
|
project_id = data.get("projectId")
|
|
location = data.get("location", "us-central1")
|
|
access_token = data.get("accessToken")
|
|
|
|
if not project_id or not access_token:
|
|
return jsonify({"valid": False, "message": "Project ID and token required"})
|
|
|
|
try:
|
|
creds = Credentials(token=access_token)
|
|
|
|
test_client = genai.Client(
|
|
vertexai=True, project=project_id, location=location, credentials=creds
|
|
)
|
|
|
|
test_client.models.generate_content(
|
|
model="gemini-2.0-flash", contents="Say 'ok' and nothing else"
|
|
)
|
|
|
|
session_credentials["oauth"] = {
|
|
"credentials": creds,
|
|
"project_id": project_id,
|
|
"location": location,
|
|
"access_token": access_token,
|
|
}
|
|
|
|
logging.info(f"User authenticated with project: {project_id}")
|
|
return jsonify({"valid": True, "project": project_id})
|
|
except Exception as e:
|
|
logging.error(f"Token validation failed: {e}")
|
|
return jsonify({"valid": False, "message": str(e)})
|
|
|
|
|
|
@app.route("/api/auth-status", methods=["GET"])
|
|
def auth_status():
|
|
"""Get current authentication status."""
|
|
if "oauth" in session_credentials:
|
|
return jsonify(
|
|
{"authenticated": True, "project": session_credentials["oauth"]["project_id"]}
|
|
)
|
|
return jsonify(
|
|
{"authenticated": False, "project": DEFAULT_PROJECT_ID, "using": "default"}
|
|
)
|
|
|
|
|
|
@app.route("/api/logout", methods=["POST"])
|
|
def logout():
|
|
"""Log out the current user."""
|
|
global session_credentials
|
|
if "oauth" in session_credentials:
|
|
del session_credentials["oauth"]
|
|
logging.info("User logged out, using default credentials")
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/set-user-name", methods=["POST"])
|
|
def set_user_name_route():
|
|
"""Set the user's display name."""
|
|
global user_name
|
|
user_name = request.json.get("name", "User")
|
|
return jsonify({"success": True, "name": user_name})
|
|
|
|
|
|
@app.route("/api/get-user-name", methods=["GET"])
|
|
def get_user_name_route():
|
|
"""Get the user's display name."""
|
|
return jsonify({"name": user_name})
|
|
|
|
|
|
@app.route("/api/get-system-instructions", methods=["GET"])
|
|
def get_system_instructions():
|
|
"""Get custom system instructions."""
|
|
return jsonify({"instructions": custom_system_instructions})
|
|
|
|
|
|
@app.route("/api/set-system-instructions", methods=["POST"])
|
|
def set_system_instructions():
|
|
"""Set custom system instructions."""
|
|
global custom_system_instructions
|
|
custom_system_instructions = request.json.get("instructions", "").strip()
|
|
return jsonify({"success": True, "instructions": custom_system_instructions})
|
|
|
|
|
|
@app.route("/api/clear-all", methods=["POST"])
|
|
def clear_all():
|
|
"""Clear all session data."""
|
|
global file_history
|
|
session_id = request.json.get("session_id", "default")
|
|
if session_id in session_states:
|
|
del session_states[session_id]
|
|
if session_id in session_handles:
|
|
del session_handles[session_id]
|
|
file_history = [f for f in file_history if f.get("session_id") != session_id]
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@app.route("/api/list-files", methods=["GET"])
|
|
def api_list_files():
|
|
"""List all files for a session."""
|
|
session_id = request.args.get("session_id", "default")
|
|
files = [f for f in file_history if f.get("session_id") == session_id]
|
|
return jsonify({"files": files})
|
|
|
|
|
|
@app.route("/")
|
|
def serve_home():
|
|
"""Serve the main page."""
|
|
return send_from_directory(".", "index.html")
|
|
|
|
|
|
@app.route("/outputs/<path:filename>")
|
|
def serve_output(filename):
|
|
"""Serve generated output files."""
|
|
return send_from_directory("outputs", filename)
|
|
|
|
|
|
@app.route("/src/<path:filename>")
|
|
def serve_src_files(filename):
|
|
"""Serve source files."""
|
|
return send_from_directory("src", filename)
|
|
|
|
|
|
@app.route("/style.css")
|
|
def serve_style():
|
|
"""Serve the stylesheet."""
|
|
if Path("style.css").exists():
|
|
return send_from_directory(".", "style.css")
|
|
return "", 200
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import shutil
|
|
|
|
print("=" * 50)
|
|
print("GenMedia Live")
|
|
print(f"Project: {DEFAULT_PROJECT_ID}")
|
|
print(f"ffmpeg: {'available' if shutil.which('ffmpeg') else 'NOT FOUND'}")
|
|
print("=" * 50)
|
|
|
|
socketio.run(app, host="0.0.0.0", port=8080, debug=False)
|