1. Einführung
Umfang
Der Formula E AI Kiosk ist eine interaktive Anwendung, die auf der Gemini Platform und der Google GenAI Interactions API basiert. Nutzer machen ein Foto von sich, wählen ein Formel‑E-Szenario aus (z. B. Feiern auf dem Podium oder Start aus der Boxengasse) und lassen sich von Gemini in einen Formel‑E-Rennfahrer neben einem Gen4-Elektrorennwagen verwandeln.

Fokus auf interaktives Lernen
In diesem Codelab schreiben Sie die grundlegende Logik für generative KI mit der Interactions API auf der Gemini Platform (enterprise=True). Sie füllen wichtige Codelücken für:
- Client-Initialisierung:
genai.Client(enterprise=True)wird für die Gemini Platform initialisiert. - Gemini 3.8 Flash: Generiert einen umfangreichen Prompt aus dem Nutzerfoto mithilfe von dict-basierter multimodaler Eingabe (
[{"type": "text", ...}, {"type": "image", ...}]) undgeneration_config={"thinking_level": "high", "temperature": 0.2}. - Nano Banana 2/2.1 (
gemini-3.1-flash-image or gemini-nano-banana-2.1): Erstellung von Fahreravataren und Gen4-Lackierungen im Image-to-Image-Verfahren mithilfe des erweiterten Prompts. - Gemini Omni 1.1 Flash (
gemini-omni-1.1-flash-preview): Asynchrone Videogenerierung mitbackground=Trueund Abfrage vonclient.interactions.get().
Keine Sorge, wenn du mal nicht weiterkommst Jede Programmierübung enthält einen Block 💡 Lösung und Erklärung. Wenn Probleme auftreten, scrollen Sie nach unten, um den vollständigen Code und die Parameterdetails aufzurufen.
Die Schritte, die nicht mit KI zu tun haben (Shell-Gerüst, Docker-Konfigurationen und Cloud Run-Bereitstellung), bleiben einfache Befehle, die Sie kopieren und einfügen können. So können Sie sich ganz auf die Interactions API konzentrieren.
Core Branding & Assets
Asset | Beschreibung | URL der Quelle |
Gen4-Formel-E-Auto | Offizielle Referenz für das vollelektrische Rennauto der 4. Generation |
|
Logo von Formula E und Google Cloud | Transparentes Co-Branding-Logo |
|
Schriftart Google Sans Regular | Offizielle Markentypografie |
|
Schriftart Google Sans Medium | Offizielle Markentypografie |
|
2. 🛠️ Umgebung einrichten und Projektgerüst erstellen
Sie erstellen das Arbeitsbereichsverzeichnis und die Projektdateien direkt in Cloud Shell mit modularen CLI-Befehlen.
1. Cloud Shell öffnen und Projektverzeichnis erstellen
👉 Öffnen Sie den Cloud Shell-Editor in Ihrem Browser.
👉💻 Arbeitsbereichsstruktur erstellen:
mkdir -p ~/fe-genmedia-kiosk/{backend/app/{routers,services,prompts},frontend/{public/fonts,src},scripts}
cd ~/fe-genmedia-kiosk
2. Erforderliche Google Cloud APIs aktivieren
👉💻 Legen Sie Ihre aktuelle Projekt-ID fest und aktivieren Sie die erforderlichen Dienste:
export PROJECT_ID=$(gcloud config get-value project)
echo "Using Project ID: $PROJECT_ID"
gcloud services enable \
run.googleapis.com \
cloudbuild.googleapis.com \
aiplatform.googleapis.com \
storage.googleapis.com \
artifactregistry.googleapis.com \
compute.googleapis.com
3. Dienstkonto erstellen und IAM-Berechtigungen konfigurieren
Hinweis: Wenn Sie ein GCP-Konto einer Organisation verwenden und kein Dienstkonto erstellen können, haben Sie stattdessen ein Compute Engine-Standarddienstkonto mit vorkonfigurierten Berechtigungen.
👉💻 Führen Sie die Konfigurationsbefehle für das Laufzeitdienstkonto und Cloud Build aus:
# 1. Configure Cloud Build permissions for default Compute Service Account
# (Cloud Run source deployments use Cloud Build and the default Compute Engine SA)
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format="value(projectNumber)")
for role in roles/storage.admin roles/logging.logWriter roles/artifactregistry.writer; do
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
--role="$role"
done
# 2. Create Cloud Storage Bucket
export BUCKET_NAME="fe-genmedia-kiosk-${PROJECT_ID}"
gcloud storage buckets create gs://${BUCKET_NAME} --location=us-central1
# 3. Configure CORS for web browser uploads
cat << 'EOF' > cors.json
[
{
"origin": ["*"],
"method": ["GET", "POST", "PUT", "DELETE", "HEAD"],
"responseHeader": ["*"],
"maxAgeSeconds": 3600
}
]
EOF
gcloud storage buckets update gs://${BUCKET_NAME} --cors-file=cors.json
4. Formula E-Marken-Assets herunterladen
👉💻 Rufen Sie die Assets mit Co-Branding in Ihrem öffentlichen Ordner ab:
cd ~/fe-genmedia-kiosk/frontend/public
# Download Co-Branded Logo
curl -o formula_e_gc_logo.webp "https://storage.googleapis.com/fe-demo-branding/formula_e_gc_logo_transparent.webp"
# Download Gen4 Reference Car Image
curl -o gen4_car_reference.jpg "https://resources.formula-e.pulselive.com/photo-resources/2026/04/21/7c76418b-85af-492c-8ba6-81dabd6f3b4c/JAZ_3586-FE-2026-Gen4-Unleashed-JazzVian-for-ParcFerme.jpg?width=1280"
# Download Typography
curl -o fonts/GoogleSans-Regular.ttf "https://storage.googleapis.com/fe-demo-branding/GoogleSans-Regular.ttf"
curl -o fonts/GoogleSans-Medium.ttf "https://storage.googleapis.com/fe-demo-branding/GoogleSans-Medium.ttf"
cd ~/fe-genmedia-kiosk
5. Umgebung und Abhängigkeitskonfiguration erstellen
👉💻 .env und backend/pyproject.toml erstellen:
cat << EOF > .env
GOOGLE_CLOUD_PROJECT=${PROJECT_ID}
GOOGLE_CLOUD_LOCATION=global
GCS_BUCKET=${BUCKET_NAME}
GCS_SIGNING_SA=fe-kiosk-sa@${PROJECT_ID}.iam.gserviceaccount.com
EOF
cat << 'EOF' > backend/pyproject.toml
[project]
name = "fe-genmedia-backend"
version = "1.0.0"
description = "Formula E Generative Media Kiosk Backend"
dependencies = [
"fastapi>=0.110.0",
"uvicorn[standard]>=0.28.0",
"google-genai>=2.22.0",
"google-cloud-storage>=2.14.0",
"pydantic>=2.6.0",
"python-multipart>=0.0.9",
"pillow>=10.2.0",
"aiofiles>=23.2.0"
]
EOF
cat << 'EOF' > backend/app/config.py
import os
from pydantic import BaseModel
class Settings(BaseModel):
project_id: str = os.getenv("GOOGLE_CLOUD_PROJECT", "")
location: str = os.getenv("GOOGLE_CLOUD_LOCATION", "global")
gcs_bucket: str = os.getenv("GCS_BUCKET", "")
settings = Settings()
EOF
6. Seed Starter Gemini-Dienst (backend/app/services/gemini_service.py)
👉💻 Erstellen Sie die Starter-Dienstdatei mit dem Gerüst und den TODO-Lücken für die praktischen Aufgaben und kopieren Sie den Starter-Code:
touch backend/app/services/gemini_service.py
import base64
import json
import time
from google import genai
from google.genai import types
from google.cloud import storage
from app.config import settings
# =====================================================================
# TODO 1: Initialize the Google GenAI Client for Gemini Platform
# Use genai.Client() with enterprise=True, project, and location from settings.
# =====================================================================
client = None # REPLACE THIS WITH YOUR CODE
def upload_to_gcs(data_bytes: bytes, filename: str, content_type: str) -> str:
"""Uploads binary data to Cloud Storage bucket and returns public URL."""
storage_client = storage.Client()
bucket = storage_client.bucket(settings.gcs_bucket)
blob = bucket.blob(filename)
blob.upload_from_string(data_bytes, content_type=content_type)
return f"https://storage.googleapis.com/{settings.gcs_bucket}/{filename}"
def generate_driver_prompt(image_bytes: bytes, scene_option: str) -> str:
"""Uses Gemini 3.8 Flash via Interactions API to craft a detailed prompt from the driver photo."""
base64_image = base64.b64encode(image_bytes).decode("utf-8")
# =====================================================================
# TODO 2: Call client.interactions.create()
# Model: "gemini-3.8-flash"
# Input: list of dicts [{"type": "text", ...}, {"type": "image", "bytes": ..., "mime_type": "image/jpeg"}]
# Generation Config: {"thinking_level": "high", "temperature": 0.2}
# =====================================================================
interaction = None # REPLACE THIS WITH YOUR CODE
return interaction.output_text
def generate_driver_avatar(driver_prompt: str, image_bytes: bytes) -> bytes:
"""Uses Nano Banana 2 (gemini-3.1-flash-image) to generate stylized driver avatar."""
base64_image = base64.b64encode(image_bytes).decode("utf-8")
# =====================================================================
# TODO 3: Call client.interactions.create() for image generation
# Model: "gemini-3.1-flash-image"
# Input: list of dicts containing prompt text and input driver image
# Generation Config: {"aspect_ratio": "16:9", "output_mime_type": "image/png"}
# =====================================================================
interaction = None # REPLACE THIS WITH YOUR CODE
if interaction and interaction.output_image:
return base64.b64decode(interaction.output_image.data)
raise ValueError("Failed to generate avatar image.")
def generate_formula_e_video(driver_prompt: str, avatar_bytes: bytes) -> bytes:
"""Uses Gemini Omni Flash to generate an 8-second cinematic Formula E video."""
base64_avatar = base64.b64encode(avatar_bytes).decode("utf-8")
# =====================================================================
# TODO 4a: Start async video generation with 'gemini-omni-1.1-flash-preview'
# Input: list of dicts with avatar image and prompt text. Set background=True!
# =====================================================================
interaction = None # REPLACE THIS WITH YOUR CODE
# =====================================================================
# TODO 4b: Write polling loop with client.interactions.get(interaction.id)
# Loop every 5 seconds until status == "completed" or "failed".
# Return base64.b64decode(status_check.output_video.data)
# =====================================================================
while True:
# REPLACE THIS LOOP BODY WITH YOUR CODE
time.sleep(5)
3. ⚡ Interactions API und generative Modelle implementieren
In diesem Abschnitt schreiben Sie die Kernlogik für generative KI in backend/app/services/gemini_service.py. Dazu verwenden Sie die Interactions API und die GenerateContent API auf der Gemini Platform.
Öffnen Sie backend/app/services/gemini_service.py im Cloud Shell-Editor. Sie finden vier TODO-Aufgaben, die Sie erledigen müssen.
Aufgabe 1: Gemini Enterprise-Client initialisieren
Öffnen Sie backend/app/services/gemini_service.py. Führen Sie TODO 1 aus, indem Sie die für Gemini Enterprise (enterprise=True) konfigurierte genai.Client initialisieren.
import base64
import json
import time
from google import genai
from google.genai import types
from app.config import settings
# =====================================================================
# TODO 1: Initialize the Google GenAI Client for Gemini Platform
# Use genai.Client() with enterprise=True, project, and location from settings.
# =====================================================================
client = None # REPLACE THIS WITH YOUR CODE
💡 Brauchen Sie einen Tipp oder kommen Sie nicht weiter? Scrollen Sie nach unten, um die Lösung für TODO 1 zu sehen.
client = genai.Client(
enterprise=True,
project=settings.project_id,
location=settings.location
)
Aufgabe 2: Driver-Prompt mit gemini-3.8-flash erweitern
Führen Sie TODO 2 in generate_driver_prompt() aus. Verwenden Sie gemini-3.8-flash, um das Foto des Fahrers zu analysieren und einen detaillierten Prompt für Nano Banana 2 zu schreiben. Übergeben Sie eine Liste von Wörterbüchern an input und übergeben Sie generation_config={"thinking_level": "high", "temperature": 0.2}.
def generate_driver_prompt(image_bytes: bytes, scene_option: str) -> str:
"""Uses Gemini 3.8 Flash via Interactions API to craft a detailed prompt from the driver photo."""
base64_image = base64.b64encode(image_bytes).decode("utf-8")
# =====================================================================
# TODO 2: Call client.interactions.create()
# Model: "gemini-3.8-flash"
# Input: list of dicts [{"type": "text", ...}, {"type": "image", "bytes": ..., "mime_type": "image/jpeg"}]
# Generation Config: {"thinking_level": "high", "temperature": 0.2}
# =====================================================================
interaction = None # REPLACE THIS WITH YOUR CODE
return interaction.output_text
💡 Brauchen Sie einen Tipp oder kommen Sie nicht weiter? Klicken Sie, um die Lösung für TODO 2 zu maximieren.
interaction = client.interactions.create(
model="gemini-3.8-flash",
input=[
{"type": "text", "text": f"Analyze this photo and write a prompt describing this person as a Formula E race driver in a high-tech suit for a {scene_option} scene beside a Gen4 car."},
{"type": "image", "bytes": base64_image, "mime_type": "image/jpeg"}
],
generation_config={
"thinking_level": "high",
"temperature": 0.2
}
)
Aufgabe 3: Avatar-Generierung mit Nano Banana 2 (gemini-3.1-flash-image über die GenerateContent API)
Führen Sie TODO 3 in generate_driver_avatar() aus. Rufen Sie client.models.generate_content() mit gemini-3.1-flash-image (Nano Banana 2) auf, um den stilisierten Formel-E-Fahreravatar und das Gen4-Auto zu rendern.
def generate_driver_avatar(driver_prompt: str, image_bytes: bytes) -> bytes:
"""Uses Nano Banana 2 (gemini-3.1-flash-image) to generate stylized driver avatar."""
text_parts = types.Part.from_text(text=f"Generate a stylized Formula E driver portrait: {driver_prompt}")
image_parts = types.Part.from_bytes(data=image_bytes,
mime_type='image/jpeg',)
gen4_image_parts = types.Part.from_uri(file_uri="https://coffeecornermotorsport.com/wp-content/uploads/2026/04/m37258_GEN4-HERO3.webp", mime_type="image/webp")
# =====================================================================
# TODO 3: Call client.models.generate_content() for image generation
# Model: "gemini-3.1-flash-image" or "gemini-nano-banana-2.1"
# Content: A list with the parts
# Generation Config: Image Response Modality and 16:9 Aspect ratio
# =====================================================================
response = None # REPLACE THIS WITH YOUR CODE
generated_bytes = None
if response.candidates and response.candidates[0].content.parts:
for part in response.candidates[0].content.parts:
if part.inline_data and part.inline_data.data:
generated_bytes = part.inline_data.data
break
return base64.b64encode(generated_bytes).decode('utf-8')
💡 Brauchen Sie einen Tipp oder kommen Sie nicht weiter? Scrollen Sie nach unten, um die Lösung für TODO 3 zu sehen.
response = client.models.generate_content(
model="gemini-3.1-flash-image", # Or "gemini-nano-banana-2.1"
content=[text_parts, image_parts, gen4_image_parts],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(aspect_ratio="16:9"),
)
)
Aufgabe 4: Videogenerierung mit Gemini Omni 1.1 Flash
Führen Sie TODO 4 in generate_formula_e_video() aus. Starten Sie eine asynchrone Videointeraktion mit gemini-omni-1.1-flash-preview mithilfe von background=True, fragen Sie client.interactions.get() ab, bis der Status „Abgeschlossen“ lautet, und geben Sie die Base64-decodierten Videobytes (output_video.data) zurück.
def generate_formula_e_video(driver_prompt: str, avatar_bytes: bytes) -> bytes:
"""Uses Gemini Omni 1.1 Flash to generate an 8-second cinematic Formula E video."""
base64_avatar = base64.b64encode(avatar_bytes).decode("utf-8")
# =====================================================================
# TODO 4a: Start async video generation with 'gemini-omni-1.1-flash-preview'
# Input: list of dicts with avatar image and prompt text. Set background=True!
# =====================================================================
interaction = None # REPLACE THIS WITH YOUR CODE
# =====================================================================
# TODO 4b: Write polling loop with client.interactions.get(interaction.id)
# Loop every 5 seconds until status == "completed" or "failed".
# Return base64.b64decode(status_check.output_video.data)
# =====================================================================
while True:
# REPLACE THIS LOOP BODY WITH YOUR CODE
time.sleep(5)
💡 Brauchen Sie einen Tipp oder kommen Sie nicht weiter? Klicken Sie, um die Lösung für TODO 4 zu maximieren.
# TODO 4a Solution:
interaction = client.interactions.create(
model="gemini-omni-1.1-flash-preview",
input=[
{"type": "text", "text": f"Generate an 8-second 60fps cinematic video: {driver_prompt}. Formula E racing atmosphere."},
{"type": "image", "bytes": base64_avatar, "mime_type": "image/png"}
],
background=True
)
# TODO 4b Solution:
while True:
status_check = client.interactions.get(interaction.id)
if status_check.status == "completed":
return base64.b64decode(status_check.output_video.data)
elif status_check.status in ["failed", "cancelled"]:
raise RuntimeError(f"Video generation failed: {status_check.status}")
time.sleep(5)
4. 🏗️ Hauptanwendung und Frontend zusammenstellen
Erstellen Sie nun die restlichen Wrapper-Dateien für die Anwendung, die API-Endpunkte und die React-Oberfläche.
[!WICHTIG] Führen Sie alle Befehle zur Dateierstellung in diesem Abschnitt im Stammverzeichnis des Arbeitsbereichs (~/fe-genmedia-kiosk) aus.
👉💻 Wechseln Sie zum Stammverzeichnis des Projekts:
cd ~/fe-genmedia-kiosk
1. FastAPI-Hauptanwendung (backend/app/main.py)
👉💻 Erstellen Sie backend/app/main.py und kopieren Sie den folgenden Python-Code:
touch backend/app/main.py
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import asyncio
import uuid
from app.services.gemini_service import (
generate_driver_prompt,
generate_driver_avatar,
generate_formula_e_video,
upload_to_gcs
)
app = FastAPI(title="Formula E AI Kiosk API")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
jobs_db = {}
class JobStatus(BaseModel):
job_id: str
status: str
stage: str
result_url: str | None = None
@app.get("/api/health")
def health_check():
return {"status": "ok", "experience": "Formula E Gen4 AI Kiosk"}
@app.post("/api/process-driver")
async def process_driver(
file: UploadFile = File(...),
option: str = Form("podium_celebration")
):
job_id = str(uuid.uuid4())
image_bytes = await file.read()
jobs_db[job_id] = {
"status": "processing",
"stage": "generating_prompt",
"result_url": None
}
asyncio.create_task(run_kiosk_pipeline(job_id, image_bytes, option))
return {"job_id": job_id, "status": "queued"}
async def run_kiosk_pipeline(job_id: str, image_bytes: bytes, option: str):
try:
jobs_db[job_id]["stage"] = "Writing driver prompt with Gemini 3.8 Flash..."
driver_prompt = generate_driver_prompt(image_bytes, option)
jobs_db[job_id]["stage"] = "Rendering Nano Banana 2 avatar..."
avatar_bytes = generate_driver_avatar(driver_prompt, image_bytes)
jobs_db[job_id]["stage"] = "Generating Gemini Omni Flash video..."
video_bytes = generate_formula_e_video(driver_prompt, avatar_bytes)
jobs_db[job_id]["stage"] = "Publishing generated video to Cloud Storage..."
result_url = upload_to_gcs(video_bytes, f"videos/{job_id}.mp4", "video/mp4")
jobs_db[job_id]["status"] = "completed"
jobs_db[job_id]["stage"] = "Done"
jobs_db[job_id]["result_url"] = result_url
except Exception as e:
jobs_db[job_id]["status"] = "failed"
jobs_db[job_id]["stage"] = str(e)
@app.get("/api/job/{job_id}", response_model=JobStatus)
def get_job_status(job_id: str):
if job_id not in jobs_db:
raise HTTPException(status_code=404, detail="Job not found")
data = jobs_db[job_id]
return JobStatus(
job_id=job_id,
status=data["status"],
stage=data["stage"],
result_url=data.get("result_url")
)
2. Backend-Dockerfile (backend/Dockerfile)
👉💻 Erstellen Sie die Konfiguration für den Backend-Container:
cat << 'EOF' > backend/Dockerfile
FROM python:3.11-slim
RUN apt-get update && apt-get install -y ffmpeg libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir .
COPY app/ ./app/
EXPOSE 8080
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
EOF
3. Frontend-React-Anwendung und -Konfiguration
Erstellen Sie die Frontend-Konfiguration und die Quelldateien Schritt für Schritt, damit jede Datei in Cloud Shell ohne Probleme mit dem Einfügepuffer generiert wird.
3.1 Paketmanifest (frontend/package.json)
👉💻 Erstellen Sie die Definition der Frontend-Abhängigkeiten:
cat << 'EOF' > frontend/package.json
{
"name": "fe-genmedia-frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"typescript": "^5.2.2",
"vite": "^5.1.6"
}
}
EOF
3.2 TypeScript-Konfiguration (frontend/tsconfig.json)
👉💻 Erstellen Sie frontend/tsconfig.json, damit der TypeScript-Compiler (tsc) das Projekt validieren und erstellen kann:
cat << 'EOF' > frontend/tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": false
},
"include": ["src"]
}
EOF
3.3 Vite-Build-Konfiguration (frontend/vite.config.ts)
👉💻 Vite-Konfiguration erstellen:
cat << 'EOF' > frontend/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
port: 8080,
host: true
}
});
EOF
3.4 HTML-Einstiegspunkt (frontend/index.html)
👉💻 Erstellen Sie den HTML-Stammeinstiegspunkt:
cat << 'EOF' > frontend/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Formula E AI Kiosk</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
EOF
3.5 Marken-Stylesheet (frontend/src/index.css)
👉💻 Erstelle das benutzerdefinierte dunkle Design für die Formel E:
cat << 'EOF' > frontend/src/index.css
@font-face {
font-family: 'Google Sans';
src: url('/fonts/GoogleSans-Regular.ttf') format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Google Sans';
src: url('/fonts/GoogleSans-Medium.ttf') format('truetype');
font-weight: 500;
}
body {
margin: 0;
font-family: 'Google Sans', sans-serif;
background-color: #0b0e14;
color: #ffffff;
}
.fe-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.fe-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: rgba(15, 23, 42, 0.8);
border-bottom: 2px solid #00e5ff;
}
.fe-btn {
background: linear-gradient(135deg, #00e5ff 0%, #0066ff 100%);
color: #000;
font-weight: bold;
padding: 0.8rem 2rem;
border: none;
border-radius: 30px;
cursor: pointer;
font-size: 1.1rem;
transition: all 0.2s ease;
}
.fe-btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 229, 255, 0.4);
}
.fe-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.fe-btn-secondary {
background: rgba(30, 41, 59, 0.8);
color: #00e5ff;
border: 1px solid #00e5ff;
font-weight: bold;
padding: 0.6rem 1.6rem;
border-radius: 30px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s ease;
}
.fe-btn-secondary:hover:not(:disabled) {
background: rgba(0, 229, 255, 0.15);
box-shadow: 0 0 10px rgba(0, 229, 255, 0.3);
}
.fe-card {
background: #151c28;
border: 1px solid #1e293b;
border-radius: 16px;
padding: 2rem;
margin: 1.5rem auto;
max-width: 720px;
}
.fe-tabs {
display: flex;
justify-content: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.fe-tab-btn {
background: #0f172a;
border: 1px solid #334155;
color: #94a3b8;
font-size: 1rem;
font-weight: 500;
padding: 0.6rem 1.4rem;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s ease;
}
.fe-tab-btn.active {
background: #1e293b;
border-color: #00e5ff;
color: #00e5ff;
box-shadow: 0 0 10px rgba(0, 229, 255, 0.2);
}
.fe-camera-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 1.5rem auto;
}
.fe-camera-feed-box {
position: relative;
width: 100%;
max-width: 480px;
aspect-ratio: 4/3;
background: #0b0e14;
border-radius: 12px;
overflow: hidden;
border: 2px solid #334155;
display: flex;
align-items: center;
justify-content: center;
}
.fe-video {
width: 100%;
height: 100%;
object-fit: cover;
transform: scaleX(-1);
}
.fe-captured-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 12px;
}
.fe-alert {
background: rgba(239, 68, 68, 0.15);
border: 1px solid #ef4444;
color: #fca5a5;
padding: 0.8rem 1.2rem;
border-radius: 8px;
margin: 1rem auto;
max-width: 480px;
font-size: 0.95rem;
}
.fe-file-picker {
margin: 1.5rem 0;
}
.fe-file-picker input[type="file"] {
background: #0f172a;
border: 1px dashed #334155;
padding: 1.5rem;
border-radius: 12px;
color: #94a3b8;
width: 80%;
max-width: 400px;
cursor: pointer;
}
.fe-file-picker input[type="file"]:hover {
border-color: #00e5ff;
color: #ffffff;
}
EOF
3.6 React-Stammkomponente (frontend/src/main.tsx)
👉💻 Erstellen Sie die Bootstrap-Datei für die React-Anwendung:
cat << 'EOF' > frontend/src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
EOF
3.7 Benutzeroberfläche für interaktive Kioske (frontend/src/App.tsx)
👉💻 Erstellen Sie die interaktive Hauptoberfläche des Kiosks, indem Sie die Datei „App.tsx“ erstellen und den React-Code kopieren und einfügen:
touch frontend/src/App.tsx
import React, { useState, useRef, useEffect, useCallback } from 'react';
import './index.css';
export default function App() {
const [inputMode, setInputMode] = useState<'camera' | 'upload'>('camera');
const [file, setFile] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [cameraActive, setCameraActive] = useState<boolean>(false);
const [cameraError, setCameraError] = useState<string | null>(null);
const [option, setOption] = useState('podium_celebration');
const [jobId, setJobId] = useState<string | null>(null);
const [status, setStatus] = useState<string>('');
const [stage, setStage] = useState<string>('');
const [resultUrl, setResultUrl] = useState<string | null>(null);
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const stopCamera = useCallback(() => {
if (streamRef.current) {
streamRef.current.getTracks().forEach((track) => track.stop());
streamRef.current = null;
}
if (videoRef.current) {
videoRef.current.srcObject = null;
}
setCameraActive(false);
}, []);
const startCamera = useCallback(async () => {
stopCamera();
setCameraError(null);
try {
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error('Camera is not supported on this browser or device.');
}
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
facingMode: 'user',
},
audio: false,
});
streamRef.current = stream;
if (videoRef.current) {
videoRef.current.srcObject = stream;
await videoRef.current.play();
}
setCameraActive(true);
} catch (err: any) {
console.error('Error accessing camera:', err);
setCameraError(err.message || 'Unable to access camera. Please check permissions or switch to file upload.');
setCameraActive(false);
}
}, [stopCamera]);
useEffect(() => {
if (inputMode === 'camera' && !file && !jobId) {
startCamera();
} else {
stopCamera();
}
return () => {
stopCamera();
};
}, [inputMode, file, jobId, startCamera, stopCamera]);
const handleCapture = () => {
const video = videoRef.current;
const canvas = canvasRef.current;
if (!video || !canvas) return;
const width = video.videoWidth || 640;
const height = video.videoHeight || 480;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Flip horizontally to match selfie preview
ctx.translate(width, 0);
ctx.scale(-1, 1);
ctx.drawImage(video, 0, 0, width, height);
canvas.toBlob((blob) => {
if (!blob) return;
const capturedFile = new File([blob], `driver_photo_${Date.now()}.jpg`, {
type: 'image/jpeg',
});
setFile(capturedFile);
setPreviewUrl(URL.createObjectURL(blob));
stopCamera();
}, 'image/jpeg', 0.95);
};
const handleRetake = () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setFile(null);
setPreviewUrl(null);
if (inputMode === 'camera') {
startCamera();
}
};
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const selected = e.target.files?.[0] || null;
if (selected) {
setFile(selected);
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setPreviewUrl(URL.createObjectURL(selected));
}
};
const handleModeChange = (mode: 'camera' | 'upload') => {
if (mode === inputMode) return;
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setFile(null);
setPreviewUrl(null);
setInputMode(mode);
};
const handleUpload = async () => {
if (!file) return;
stopCamera();
const formData = new FormData();
formData.append('file', file);
formData.append('option', option);
const apiBase = (import.meta as any).env.VITE_API_BASE || '';
const res = await fetch(`${apiBase}/api/process-driver`, { method: 'POST', body: formData });
const data = await res.json();
setJobId(data.job_id);
pollJob(data.job_id);
};
const pollJob = (id: string) => {
const apiBase = (import.meta as any).env.VITE_API_BASE || '';
const interval = window.setInterval(async () => {
try {
const res = await fetch(`${apiBase}/api/job/${id}`);
const data = await res.json();
setStatus(data.status);
setStage(data.stage);
if (data.status === 'completed') {
setResultUrl(data.result_url);
clearInterval(interval);
} else if (data.status === 'failed') {
clearInterval(interval);
}
} catch (err) {
console.error('Error polling job:', err);
}
}, 3000);
};
const handleReset = () => {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
}
setFile(null);
setPreviewUrl(null);
setJobId(null);
setStatus('');
setStage('');
setResultUrl(null);
};
return (
<div>
<header className="fe-header">
<img src="/formula_e_gc_logo.webp" alt="Formula E Google Cloud" height="45" />
<h2>GEN4 AI DRIVER EXPERIENCE</h2>
</header>
<main className="fe-container">
<h1>Transform into a Formula E Driver</h1>
<p>Powered by Gemini Platform & Google Cloud Run</p>
{!jobId ? (
<div className="fe-card">
<h3>Step 1: Take or Upload Your Driver Photo</h3>
<div className="fe-tabs">
<button
type="button"
className={`fe-tab-btn ${inputMode === 'camera' ? 'active' : ''}`}
onClick={() => handleModeChange('camera')}
>
📷 Take Photo
</button>
<button
type="button"
className={`fe-tab-btn ${inputMode === 'upload' ? 'active' : ''}`}
onClick={() => handleModeChange('upload')}
>
📁 Upload Photo
</button>
</div>
{/* Mode: Camera */}
{inputMode === 'camera' && (
<div className="fe-camera-container">
{cameraError && (
<div className="fe-alert">
<p>{cameraError}</p>
<button type="button" className="fe-btn-secondary" onClick={startCamera} style={ { marginTop: '0.5rem' } }>
Try Camera Again
</button>
</div>
)}
<div className="fe-camera-feed-box">
{!file ? (
<video
ref={videoRef}
className="fe-video"
autoPlay
playsInline
muted
/>
) : (
previewUrl && (
<img src={previewUrl} alt="Captured Driver" className="fe-captured-img" />
)
)}
</div>
<canvas ref={canvasRef} style={ { display: 'none' } } />
<div style={ { marginTop: '1rem', display: 'flex', gap: '0.75rem', justifyContent: 'center' } }>
{!file ? (
<button
type="button"
className="fe-btn"
onClick={handleCapture}
disabled={!cameraActive}
>
📸 Snap Photo
</button>
) : (
<button
type="button"
className="fe-btn-secondary"
onClick={handleRetake}
>
🔄 Retake Photo
</button>
)}
</div>
</div>
)}
{/* Mode: Upload */}
{inputMode === 'upload' && (
<div className="fe-file-picker">
{!file ? (
<input
type="file"
accept="image/*"
onChange={handleFileChange}
/>
) : (
<div>
{previewUrl && (
<div className="fe-camera-feed-box" style={ { margin: '0 auto 1rem auto' } }>
<img src={previewUrl} alt="Uploaded Driver" className="fe-captured-img" />
</div>
)}
<button type="button" className="fe-btn-secondary" onClick={handleRetake}>
Choose Different Photo
</button>
</div>
)}
</div>
)}
<h3>Step 2: Choose Your Experience</h3>
<select
value={option}
onChange={(e) => setOption(e.target.value)}
style={ { padding: '0.6rem 1rem', fontSize: '1rem', background: '#0f172a', color: '#fff', border: '1px solid #334155', borderRadius: '8px' } }
>
<option value="podium_celebration">🏆 Podium Winner Celebration</option>
<option value="pit_lane_launch">⚡ Pit Lane Gen4 Launch</option>
</select>
<div style={ { marginTop: '2rem' } }>
<button className="fe-btn" onClick={handleUpload} disabled={!file}>
START GENERATIVE PIPELINE
</button>
</div>
</div>
) : (
<div className="fe-card">
<h2>Pipeline Progress</h2>
<p><strong>Status:</strong> {status.toUpperCase() || 'INITIALIZING...'}</p>
<p><strong>Stage:</strong> {stage || 'Starting processing...'}</p>
{resultUrl && (
<div style={ { marginTop: '2rem' } }>
<h3>Your Formula E Generative Video</h3>
<video src={resultUrl} controls autoPlay loop style={ { width: '100%', maxWidth: '720px', borderRadius: '12px' } } />
</div>
)}
{(status === 'completed' || status === 'failed') && (
<div style={ { marginTop: '2rem' } }>
<button type="button" className="fe-btn-secondary" onClick={handleReset}>
CREATE ANOTHER DRIVER
</button>
</div>
)}
</div>
)}
</main>
</div>
);
}
3.8 Nginx-Serverkonfiguration (frontend/nginx.conf)
👉💻 Erstellen Sie die Nginx-Webserverkonfiguration für das Routing der Single-Page-Anwendung:
cat << 'EOF' > frontend/nginx.conf
server {
listen 8080;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
}
EOF
3.9 Frontend-Container-Build-Einrichtung (frontend/Dockerfile und frontend/.dockerignore)
👉💻 Erstellen Sie .dockerignore und die mehrstufige Produktion Dockerfile:
cat << 'EOF' > frontend/.dockerignore
node_modules
dist
.git
EOF
touch frontend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 8080
5. 🚀 In Cloud Run bereitstellen
1. Backend-Container erstellen und bereitstellen
👉💻 Stellen Sie den Backend-Server in Cloud Run bereit:
cd ~/fe-genmedia-kiosk/backend
gcloud run deploy fe-kiosk-backend \
--source . \
--region us-central1 \
--allow-unauthenticated \
--port 8080
2. Frontend-Container erstellen und bereitstellen
Sobald das Backend fertig ist, erfassen Sie die URL, schreiben Sie sie direkt in .env.production und stellen Sie das Frontend bereit.
cd ~/fe-genmedia-kiosk/frontend
# 1. Fetch Backend URL
export BACKEND_URL=$(gcloud run services describe fe-kiosk-backend \
--region us-central1 \
--format="value(status.url)")
echo "Configuring frontend with Backend URL: $BACKEND_URL"
# 2. Write it into the Vite production environment file
echo "VITE_API_BASE=${BACKEND_URL}" > .env.production
# 3. Deploy frontend (Vite will read .env.production during build)
gcloud run deploy fe-kiosk-frontend \
--source . \
--region us-central1 \
--allow-unauthenticated \
--port 8080
6. 🏁 Formula E AI Kiosk testen und erleben
- Öffne
$FRONTEND_URLin Google Chrome. - Laden Sie ein Selfie oder Porträtfoto hoch.
- Wähle dein Rennszenario aus (Podium Winner Celebration oder Pit Lane Gen4 Launch).
- Klicken Sie auf GENERATIVE PIPELINE STARTEN.
- Sehen Sie sich den Fortschritt in Echtzeit in allen drei KI-Phasen an:
- Gemini 3.8 Flash: Generierung von Driver-Prompts.
- Nano Banana 2: Benutzerdefiniertes Gen4-Auto und Rennanzug-Avatar-Rendering.
- Gemini Omni Flash: Generierung von 8 Sekunden langen HD-Videos mit 60 fps.
- Spielen Sie das fertige KI-generierte Video ab.
7. 🎉 Fazit und Bereinigung
Das haben Sie gelernt
✅ Gemini Platform & Interactions API: genai.Client(enterprise=True) initialisieren und client.interactions.create() aufrufen.
✅ Multimodales Datenschema der Interactions API: Verwendung der Liste von Wörterbüchern input und des Wörterbuchparameters generation_config.
✅ Multimodale KI-Pipeline:
gemini-3.8-flash: Generieren von benutzerdefinierten Treiber-Prompts.gemini-3.1-flash-image(Nano Banana 2): Erstellung von Fahrer-Avatar und Gen4-Lackierung.gemini-omni-1.1-flash-preview: Asynchrone Videogenerierung mit Abfrage von Hintergrundaufgaben.
✅ Serverlose Bereitstellung: Containerisierte und bereitgestellte Dienste in Google Cloud Run.
Ressourcenbereinigung
👉💻 Google Cloud-Umgebung bereinigen:
gcloud run services delete fe-kiosk-backend --region us-central1 --quiet
gcloud run services delete fe-kiosk-frontend --region us-central1 --quiet
gcloud storage rm -r gs://${BUCKET_NAME}
gcloud iam service-accounts delete fe-kiosk-sa@${PROJECT_ID}.iam.gserviceaccount.com --quiet