1. مقدمة
ما ستقوم بإنشائه
Formula E AI Kiosk هي تجربة تفاعلية مستندة إلى منصة Gemini وGoogle GenAI Interactions API. يلتقط المستخدمون صورة لأنفسهم، ويختارون سيناريو Formula E (مثل الاحتفال على منصة التتويج أو الانطلاق من حارة الصيانة)، ويسمحون لـ Gemini بتحويلهم إلى سائق سباق Formula E بجانب سيارة سباق كهربائية من الجيل الرابع.

التركيز على التعلّم التفاعلي
في هذا الدرس التطبيقي حول الترميز، ستكتب منطق الذكاء الاصطناعي التوليدي الأساسي باستخدام Interactions API على منصة Gemini (enterprise=True). ستملأ الفجوات الرئيسية في الرمز البرمجي من أجل:
- إعداد العميل: إعداد
genai.Client(enterprise=True)لمنصة Gemini. - Gemini 3.8 Flash: ينشئ طلبًا غنيًا بالمعلومات من صورة المستخدم باستخدام إدخال متعدد الوسائط مستند إلى القاموس (
[{"type": "text", ...}, {"type": "image", ...}]) وgeneration_config={"thinking_level": "high", "temperature": 0.2}. - Nano Banana 2/2.1 (
gemini-3.1-flash-image or gemini-nano-banana-2.1): إنشاء أفاتار المركبة ومظهر Gen4 من صورة إلى صورة باستخدام الطلب الموسّع. - Gemini Omni 1.1 Flash (
gemini-omni-1.1-flash-preview): إنشاء فيديوهات غير متزامنة باستخدامbackground=Trueوالاستطلاعclient.interactions.get().
لا داعي للقلق بشأن مواجهة صعوبات يتضمّن كل تمرين برمجة قسمًا بعنوان 💡 الحلّ والشرح. إذا واجهت أي مشاكل، انتقِل إلى الأسفل للاطّلاع على الرمز الكامل وتفاصيل المَعلمات.
تظل خطوات الإعداد غير المستندة إلى الذكاء الاصطناعي (مثل إنشاء هيكل واجهة الأوامر وإعدادات Docker والنشر على Cloud Run) مبسطة وتعتمد على أوامر النسخ واللصق، ما يتيح لك التركيز على إتقان استخدام Interactions API.
الهوية البصرية الأساسية ومواد العرض
مادة العرض | الوصف | عنوان URL المصدر |
سيارة Gen4 Formula E | مرجع رسمي لسيارة السباق الكهربائية بالكامل من الجيل الرابع |
|
شعار Formula E وGoogle Cloud | شعار شفاف يحمل علامة تجارية مشتركة |
|
خط Google Sans العادي | أسلوب الخط الرسمي للعلامة التجارية |
|
خط Google Sans Medium | أسلوب الخط الرسمي للعلامة التجارية |
|
2. 🛠️ إعداد البيئة وهيكل المشروع
ستنشئ دليل مساحة العمل وملفات المشروع مباشرةً في Cloud Shell باستخدام أوامر واجهة سطر الأوامر (CLI) المعيارية.
1. فتح Cloud Shell وإنشاء دليل المشروع
👉 افتح محرِّر Cloud Shell في المتصفّح.
👉💻 إنشاء بنية مساحة العمل:
mkdir -p ~/fe-genmedia-kiosk/{backend/app/{routers,services,prompts},frontend/{public/fonts,src},scripts}
cd ~/fe-genmedia-kiosk
2. تفعيل واجهات Google Cloud API المطلوبة
👉💻 اضبط رقم تعريف مشروعك الحالي وفعِّل الخدمات المطلوبة:
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- إنشاء حساب خدمة وضبط أذونات "إدارة الهوية وإمكانية الوصول"
ملاحظة: إذا كنت تستخدم حسابًا على GCP تابعًا لمؤسسة، ولا يمكنك إنشاء حساب خدمة، سيكون لديك بدلاً من ذلك حساب خدمة Compute Engine تلقائي يتضمّن أذونات تم ضبطها مسبقًا.
👉💻 شغِّل أوامر الضبط لحساب خدمة وقت التشغيل وCloud Build:
# 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
👉💻 جلب مواد العرض التي تحمل العلامة التجارية المشتركة إلى مجلدك العام:
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- إنشاء إعدادات البيئة والتبعيات
👉💻 أنشئ .env وbackend/pyproject.toml:
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 (backend/app/services/gemini_service.py)
👉💻 أنشئ ملف الخدمة الأوّلي الذي يحتوي على البنية الأساسية وفجوات TODO للتحديات العملية، ثم انسخ الرمز الأوّلي والصقه:
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 والنماذج التوليدية
في هذا القسم، ستكتب منطق الذكاء الاصطناعي التوليدي الأساسي في backend/app/services/gemini_service.py باستخدام Interactions API وGenerateContent API على منصة Gemini.
افتح backend/app/services/gemini_service.py في "محرِّر Cloud Shell". ستجد أربع فجوات في تحدي TODO لإكمالها.
التحدي 1: إعداد برنامج Gemini Enterprise
فتح "backend/app/services/gemini_service.py" أكمِل المهمة 1 من خلال إعداد genai.Client الذي تم ضبطه لـ Gemini Enterprise (enterprise=True).
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
💡 هل تحتاج إلى تلميح أو مساعدة؟ انتقِل إلى الأسفل للاطّلاع على الحلّ للمَهمّة 1
client = genai.Client(
enterprise=True,
project=settings.project_id,
location=settings.location
)
التحدي 2: توسيع نطاق الطلبات الموجَّهة إلى السائقين باستخدام gemini-3.8-flash
أكمِل TODO 2 في generate_driver_prompt(). استخدِم gemini-3.8-flash لتحليل صورة السائق وكتابة طلب مفصّل لـ Nano Banana 2. يجب تمرير قائمة من القواميس إلى input وتمرير 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
💡 هل تحتاج إلى تلميح أو مساعدة؟ انقر لتوسيع الحلّ للمهمة 2
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
}
)
التحدي 3: إنشاء أفاتار باستخدام Nano Banana 2 (gemini-3.1-flash-image عبر واجهة برمجة التطبيقات GenerateContent)
أكمِل TODO 3 في generate_driver_avatar(). استدعِ client.models.generate_content() باستخدام gemini-3.1-flash-image (Nano Banana 2) لعرض أفاتار سائق Formula E وسيارة Gen4 بأسلوب مميز.
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')
💡 هل تحتاج إلى تلميح أو مساعدة؟ انتقِل للأسفل للاطّلاع على حلّ المهمة 3
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"),
)
)
التحدي 4: إنشاء الفيديوهات باستخدام Gemini Omni 1.1 Flash
أكمِل TODO 4 في generate_formula_e_video(). بدء تفاعل فيديو غير متزامن مع gemini-omni-1.1-flash-preview باستخدام background=True، واستطلاع client.interactions.get() حتى تصبح الحالة "مكتملة"، وعرض وحدات بايت الفيديو التي تم فك ترميزها باستخدام base64 (output_video.data).
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)
💡 هل تحتاج إلى تلميح أو مساعدة؟ انقر لتوسيع الحلّ للمهمة 4
# 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. 🏗️ تجميع التطبيق الرئيسي والواجهة الأمامية
الآن، أنشئ ملفات تضمين التطبيق المتبقية ونقاط نهاية واجهة برمجة التطبيقات وواجهة React.
[!IMPORTANT] تأكَّد من تشغيل جميع أوامر إنشاء الملفات في هذا القسم من دليل مساحة العمل الجذر (~/fe-genmedia-kiosk).
👉💻 انتقِل إلى الدليل الجذري للمشروع:
cd ~/fe-genmedia-kiosk
1. تطبيق FastAPI الرئيسي (backend/app/main.py)
👉💻 أنشئ backend/app/main.py وانسخ رمز Python البرمجي أدناه والصقه:
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. ملف Dockerfile للواجهة الخلفية (backend/Dockerfile)
👉💻 إنشاء إعدادات حاوية الخلفية:
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- تطبيق React للواجهة الأمامية وإعداده
أنشئ ملفات الإعداد والملفات المصدر للواجهة الأمامية خطوة بخطوة لضمان إنشاء كل ملف بشكل صحيح في Cloud Shell بدون مشاكل في مخزن اللصق المؤقت.
3.1 بيان الحزمة (frontend/package.json)
👉💻 أنشئ تعريف التبعيات للواجهة الأمامية:
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 (frontend/tsconfig.json)
👉💻 أنشئ ملف frontend/tsconfig.json ليتمكّن برنامج تجميع TypeScript (tsc) من التحقّق من المشروع وإنشائه:
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 (frontend/vite.config.ts)
👉💻 إنشاء إعدادات Vite:
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 (frontend/index.html)
👉💻 أنشئ نقطة دخول HTML الجذرية:
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 ورقة أنماط العلامة التجارية (frontend/src/index.css)
👉💻 كيفية إنشاء مظهر "الوضع الداكن" المخصّص لسباق Formula 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 Root (frontend/src/main.tsx)
👉💻 أنشئ ملف bootstrap لتطبيق React:
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 واجهة مستخدم Kiosk التفاعلية (frontend/src/App.tsx)
👉💻 أنشئ واجهة Kiosk التفاعلية الرئيسية من خلال إنشاء App.tsx ونسخ رمز React ولصقه:
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 (frontend/nginx.conf)
👉💻 أنشئ إعدادات خادم الويب Nginx لتوجيه التطبيق من صفحة واحدة:
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/Dockerfile وfrontend/.dockerignore)
👉💻 أنشئوا .dockerignore والإنتاج المتعدّد المراحل 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- 🚀 النشر على Cloud Run
1. إنشاء حاوية الخلفية ونشرها
👉💻 نشر خادم الخلفية على Cloud Run:
cd ~/fe-genmedia-kiosk/backend
gcloud run deploy fe-kiosk-backend \
--source . \
--region us-central1 \
--allow-unauthenticated \
--port 8080
2. إنشاء حاوية الواجهة الأمامية ونشرها
بعد الانتهاء من الخلفية، التقط عنوان URL الخاص بها، واكتبه مباشرةً في .env.production وانشر الواجهة الأمامية
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 وتجربته
- افتح
$FRONTEND_URLفي Google Chrome. - حمِّل صورة سيلفي أو صورة بورتريه.
- اختَر سيناريو السباق (Podium Winner Celebration أو Pit Lane Gen4 Launch).
- انقر على بدء مسار الذكاء الاصطناعي التوليدي.
- يمكنك تتبُّع مستوى التقدّم في الوقت الفعلي في جميع مراحل الذكاء الاصطناعي الثلاث:
- Gemini 3.8 Flash: إنشاء الطلبات للسائقين.
- Nano Banana 2: عرض مخصّص لسيارة Gen4 وأفاتار بدلة السباق.
- Gemini Omni Flash: إنشاء فيديوهات بدقة عالية (HD) مدتها 8 ثوانٍ وبمعدل 60 لقطة في الثانية.
- شغِّل الفيديو النهائي الذي تم إنشاؤه.
7. 🎉 الخاتمة والتنظيف
ما تعلّمته
✅ منصة Gemini وInteractions API: تهيئة genai.Client(enterprise=True) واستدعاء client.interactions.create().
✅ مخطّط البيانات المتعدّدة الوسائط في Interactions API: استخدام قائمة القواميس input ومَعلمة القاموس generation_config
✅ مسار الذكاء الاصطناعي المتعدّد الوسائط:
gemini-3.8-flash: جارٍ إنشاء طلبات مخصّصة للسائقين.gemini-3.1-flash-image(Nano Banana 2): إنشاء أفاتار السائق ومظهر Gen4.-
gemini-omni-1.1-flash-preview: إنشاء فيديوهات بشكل غير متزامن مع استطلاع المهام في الخلفية
✅ النشر بدون خادم: تم وضع الخدمات في حاويات ونشرها على Google Cloud Run.
تنظيف الموارد
👉💻 إخلاء مساحة في بيئة Google Cloud:
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