Traducción en streaming
Pega texto, elige un idioma destino, observa cómo llega la traducción en
Traducción en streaming
Pega texto, elige un idioma destino, observa cómo llega la traducción en streaming. El streaming vale la pena siempre que la salida es lo bastante larga como para que esperar el blob completo se sintiera lento.
Córrelo
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Abre http://localhost:3000.
Para correrlo contra Gemini en su lugar:
PROVIDER=gemini docker compose up --build
Qué hay aquí
backend/ai_openai.py— parseatarget: <lang>\n---\n<text>, emite la traducción en streamingbackend/ai_gemini.py— la misma forma, otro SDKbackend/main.py— FastAPI:/api/ai(final) +/api/ai/stream(SSE)frontend/app/page.tsx— selector de idioma + textarea, escribe la salida conforme llega
Detenlo
docker compose down
Ejecútalo en tu máquina
Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.
Descargar stream-translate.zip
unzip stream-translate.zip
cd stream-translate
bash bootstrap-secrets.sh # one-time: pulls API keys into ./secrets
docker compose up --build # default provider: openai
# or: PROVIDER=gemini docker compose up --build
Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.
Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña README.
backend/ai_openai.py
"""Week 3 - Showcase 2 (OpenAI): stream-translate.
Translates pasted text into a target language and streams the
translation back. The input carries the target language in a small
header:
target: French
---
The text to translate.
`run_stream` yields tokens; `run` accumulates them for the contract.
"""
from typing import Iterator
from openai import OpenAI
_client = OpenAI()
def _parse(payload: str) -> tuple[str, str]:
"""Split the input into (target language, text).
The header `target: <language>` ends at a `---` separator. Anything
that doesn't match the header defaults to English so the showcase
never fails closed on bad input — it just translates to English.
"""
parts = payload.split("---", 1)
if len(parts) != 2:
return ("English", payload.strip())
header, text = parts
target = "English"
for line in header.splitlines():
s = line.strip()
if s.lower().startswith("target:"):
target = s.split(":", 1)[1].strip() or target
break
return (target, text.strip())
def _prompt(target: str, text: str) -> str:
return (
f"Translate the following text into {target}. Preserve meaning, "
"tone, and paragraph structure. Return only the translation; no "
"commentary, no explanations."
f"\n\n---\n{text}\n---"
)
def run_stream(payload: str) -> Iterator[str]:
target, text = _parse(payload)
# Cap output length to prevent runaway responses. 2048 tokens
# (~6k chars) is enough headroom for translations of inputs at
# the showcase's 8k-char ceiling.
stream = _client.responses.create(
model="gpt-5.4-nano", input=_prompt(target, text), stream=True,
max_output_tokens=2048,
)
for event in stream:
if event.type == "response.output_text.delta":
yield event.delta
def run(payload: str) -> str:
return "".join(run_stream(payload))
backend/ai_gemini.py
"""Week 3 - Showcase 2 (Gemini): stream-translate.
Same input shape and contract as ai_openai.py — see that file for the
input format. Differences are mechanical: Gemini's streaming method and
chunk shape.
"""
import os
from typing import Iterator
from google import genai
from google.genai import types
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
def _parse(payload: str) -> tuple[str, str]:
parts = payload.split("---", 1)
if len(parts) != 2:
return ("English", payload.strip())
header, text = parts
target = "English"
for line in header.splitlines():
s = line.strip()
if s.lower().startswith("target:"):
target = s.split(":", 1)[1].strip() or target
break
return (target, text.strip())
def _prompt(target: str, text: str) -> str:
return (
f"Translate the following text into {target}. Preserve meaning, "
"tone, and paragraph structure. Return only the translation; no "
"commentary, no explanations."
f"\n\n---\n{text}\n---"
)
def run_stream(payload: str) -> Iterator[str]:
target, text = _parse(payload)
# Cap output length to prevent runaway responses. 2048 tokens
# (~6k chars) is enough headroom for translations of inputs at
# the showcase's 8k-char ceiling.
stream = _client.models.generate_content_stream(
model="gemini-3.1-flash-lite", contents=_prompt(target, text),
config=types.GenerateContentConfig(max_output_tokens=2048),
)
for chunk in stream:
if chunk.text:
yield chunk.text
def run(payload: str) -> str:
return "".join(run_stream(payload))
Archivos del proyecto
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json