Compare commits

..

10 Commits

Author SHA1 Message Date
bladeclara42
f23733f78b bpom text generation 2025-11-09 10:32:35 +07:00
bladeclara42
c40cc5d2a2 fix(lyric_translator): add readme and add async request 2025-10-16 08:51:26 +07:00
bladeclara42
a2759b8169 feat: add lyric music to romanji tool 2025-08-21 15:59:02 +07:00
bladeclara42
0fd8170c5b feat: add voice transcription 2025-07-03 10:02:12 +07:00
bladeclara42
f047a3c1c2 Chore:remove unwanted files marked in .gitignore 2025-06-30 10:26:19 +07:00
bladeclara42
8e30a6ffbb update gitignore pycache 2025-06-30 09:58:32 +07:00
bladeclara42
64dc8d2517 add voice in openai api (not tested) 2025-06-30 09:57:40 +07:00
bladeclara42
fa567efd3a rename(deepseek): change name openai to deepseek client 2025-06-25 12:04:18 +07:00
bladeclara42
f59350cd73 fix (deepseek client): fix deepseek api base model api 2025-06-24 14:02:27 +07:00
bladeclara42
96a0390418 Remove .env from tracking and ignore it 2025-05-13 23:37:41 +07:00
22 changed files with 491 additions and 26 deletions

3
.env
View File

@@ -1,3 +0,0 @@
OPENAI_API_KEY=sk-e2f00b9fed01443b87407513ab14c494
OPENAI_MODEL=deepseek-chat
OPENAI_API_BASE=https://api.deepseek.com/v1 # (optional override if needed)

100
.gitignore vendored
View File

@@ -0,0 +1,100 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Virtual environment
venv/
ENV/
env/
.venv/
.ENV/
.env/
# PyInstaller
*.manifest
*.spec
# Unit test / coverage
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# MyPy
.mypy_cache/
.dmypy.json
dmypy.json
# IDEs
.vscode/
.idea/
*.sublime-project
*.sublime-workspace
# Jupyter
.ipynb_checkpoints
# Logs
*.log
# Local .env or config
.env
.env.*
# SQLite
*.sqlite3
# FastAPI docs build (if using Sphinx)
_build/
docs/_build/
".env"
# Ignore all files in the pycache folder
__pycache__
app/__pycache__/main.cpython-313.pyc
app/core/__pycache__/config.cpython-313.pyc
app/core/__pycache__/openai_voice_client.cpython-313.pyc
app/core/__pycache__/deepseek_client.cpython-313.pyc
app/core/__pycache__/openai_client.cpython-313.pyc
app/core/__pycache__/deepseek_voice_client.cpython-313.pyc
app/services/__pycache__/voice.cpython-313.pyc
app/services/__pycache__/translator.cpython-313.pyc
app/api/__pycache__/voice.cpython-313.pyc
app/api/__pycache__/translate.cpython-313.pyc

Binary file not shown.

View File

@@ -0,0 +1,15 @@
from fastapi import APIRouter
from app.models.lyric_romanji_translator import LyricRomanjiTranslatorRequest, LyricRomanjiTranslatorResponse
from app.services.lyric_romanji_translator import translate_lyric_romanji
router = APIRouter()
@router.post("/", response_model=LyricRomanjiTranslatorResponse)
async def lyric_romanji_translator(request: LyricRomanjiTranslatorRequest):
lyric_romanji = await translate_lyric_romanji(request.folder_path)
return LyricRomanjiTranslatorResponse(
results=lyric_romanji["results"],
status=lyric_romanji["status"]
)

View File

@@ -0,0 +1,10 @@
from fastapi import APIRouter
from app.models.text_generation import BPOMMobileResponseTextGenerationRequest, BPOMMobileResponseTextGenerationResponse
from app.services.text_generation import generate_text
router = APIRouter()
@router.post("/", response_model=BPOMMobileResponseTextGenerationResponse)
async def text_generator(request: BPOMMobileResponseTextGenerationRequest):
text = await generate_text(request)
return text

16
app/api/v1/voice.py Normal file
View File

@@ -0,0 +1,16 @@
from fastapi import APIRouter
from app.models.voice import VoiceRequest, VoiceResponse, TranscriptionRequest, TranscriptionResponse
from app.services.voice import generate_voice, generate_transcription
router = APIRouter()
@router.post("/", response_model=VoiceResponse)
async def voice(request: VoiceRequest):
voice = await generate_voice(request.text)
return VoiceResponse(voice=voice)
@router.post("/transcription", response_model=TranscriptionResponse)
async def transcription(request: TranscriptionRequest):
transcription = await generate_transcription(request.audio_file_path)
return TranscriptionResponse(transcription=transcription)

View File

@@ -8,3 +8,8 @@ load_dotenv()
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo")
OPENAI_API_BASE = os.getenv("OPENAI_API_BASE", "https://api.openai.com/v1")
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
DEEPSEEK_API_BASE = os.getenv("DEEPSEEK_API_BASE", "https://api.deepseek.com/v1")
DEEPSEEK_MODEL = os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
OPENAI_AUDIO_MODEL = os.getenv("OPENAI_AUDIO_MODEL", "")

View File

@@ -1,27 +1,44 @@
# app/services/openai_service.py
import openai
from openai import OpenAI
from app.core.config import OPENAI_API_KEY, OPENAI_MODEL
from app.core.config import OPENAI_API_BASE
import os
import anyio
from openai import OpenAI, OpenAIError
from app.core.config import DEEPSEEK_API_BASE, DEEPSEEK_MODEL, DEEPSEEK_API_KEY
# Set OpenAI API key from the environment
openai.api_key = OPENAI_API_KEY
openai.api_base = OPENAI_API_BASE
# Ensure the API key is properly set
if not DEEPSEEK_API_KEY:
raise ValueError("DEEPSEEK_API_KEY is not set in environment variables")
print(openai.api_key)
print(OPENAI_MODEL)
print(OPENAI_API_BASE)
# Initialize the client
client = OpenAI(
api_key=DEEPSEEK_API_KEY,
base_url=DEEPSEEK_API_BASE
)
async def chat_with_openai(messages: list):
# Use the model from environment variable or fallback to default
model = OPENAI_MODEL
client = OpenAI(api_key=openai.api_key, base_url=openai.api_base)
async def chat_with_openai(messages: list[dict[str, str]]) -> str:
if not messages:
raise ValueError("Messages list cannot be empty")
response = client.chat.completions.create(
model="deepseek-chat", # Or the model you want
messages=messages, # Update this according to the new API syntax
max_tokens=100, # Example parameter
stream=False
)
return response.choices[0].message.content
try:
# Run sync client in a thread (non-blocking for FastAPI)
response = await anyio.to_thread.run_sync(
lambda: client.chat.completions.create(
model=DEEPSEEK_MODEL,
messages=messages,
max_tokens=1000,
temperature=0.7,
stream=False
)
)
if not response.choices or not response.choices[0].message.content:
return "No response content from the model"
return response.choices[0].message.content.strip()
except OpenAIError as e:
error_msg = f"DeepSeek API Error: {str(e)}"
print(error_msg)
raise Exception(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
print(error_msg)
raise Exception(error_msg) from e

View File

@@ -0,0 +1,67 @@
import openai
from openai import OpenAI
from openai import OpenAIError
from app.core.config import OPENAI_API_KEY, OPENAI_AUDIO_MODEL, OPENAI_API_BASE
# Ensure the API key is properly set
if not OPENAI_API_KEY:
raise ValueError("OPENAI_API_KEY is not set in environment variables")
# Initialize the client with proper configuration
client = OpenAI(
api_key=OPENAI_API_KEY,
base_url=OPENAI_API_BASE
)
async def generate_voice(messages: list):
if not messages:
raise ValueError("Messages list cannot be empty")
try:
response = client.chat.completions.create(
model=OPENAI_AUDIO_MODEL,
messages=messages,
max_tokens=1000,
temperature=0.7,
stream=False
)
if not response.choices or not response.choices[0].message.content:
return "No response content from the model"
return response.choices[0].message.content
except OpenAIError as e:
error_msg = f"OpenAI API Error: {str(e)}"
print(error_msg)
raise Exception(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
print(error_msg)
raise Exception(error_msg) from e
async def generate_transcription(audio_file_path: str) -> str:
if not audio_file_path:
raise ValueError("Audio file path cannot be empty")
try:
response = client.audio.transcriptions.create(
model=OPENAI_AUDIO_MODEL,
file=audio_file_path,
response_format="text",
language="id"
)
if not response.choices or not response.choices[0].message.content:
return "No response content from the model"
return response.choices[0].message.content
except OpenAIError as e:
error_msg = f"OpenAI API Error: {str(e)}"
print(error_msg)
raise Exception(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
print(error_msg)
raise Exception(error_msg) from e

View File

@@ -1,7 +1,12 @@
from fastapi import FastAPI
from app.api.v1 import translate
from app.api.v1 import voice
from app.api.v1 import lyric_romanji_translator
app = FastAPI()
# Include your routes
app.include_router(translate.router, prefix="/api/v1/translate", tags=["translate"])
app.include_router(voice.router, prefix="/api/v1/voice", tags=["voice"])
app.include_router(lyric_romanji_translator.router, prefix="/api/v1/lyric_romanji_translator", tags=["lyric_romanji_translator"])
app.include_router(text_generator.router, prefix="/api/v1/text_generator", tags=["text_generator"])

View File

@@ -0,0 +1,14 @@
from pydantic import BaseModel
from typing import List
class LyricRomanjiTranslatorRequest(BaseModel):
folder_path: str
class FileResult(BaseModel):
file: str
processed: bool
added_lines: int
class LyricRomanjiTranslatorResponse(BaseModel):
results: List[FileResult]
status: str

View File

@@ -0,0 +1,5 @@
class BPOMMobileResponseTextGenerationRequest(BaseModel):
text: str
class BPOMMobileResponseTextGenerationRequest(BaseModel):
generated_text: str

17
app/models/voice.py Normal file
View File

@@ -0,0 +1,17 @@
from pydantic import BaseModel
from typing import Optional
# Text-to-Speech Models
class VoiceRequest(BaseModel):
text: str
class VoiceResponse(BaseModel):
voice_output: str
# Speech-to-Text Models
class TranscriptionRequest(BaseModel):
audio_file_path: str
target_language: Optional[str] = "id" # Default to English
class TranscriptionResponse(BaseModel):
text: str

View File

@@ -0,0 +1,68 @@
import os
import re
import asyncio
from app.core.deepseek_client import chat_with_openai
from app.models.lyric_romanji_translator import FileResult
semaphore = asyncio.Semaphore(5)
timestamp_pattern = re.compile(r"^\[\d{2}:\d{2}\.\d{2}\]")
def needs_romaji(lines, idx):
if idx + 1 < len(lines) and not timestamp_pattern.match(lines[idx + 1]):
return False
return True
async def get_romaji(text: str) -> str:
messages = [
{"role": "system", "content": "Convert Japanese text into romaji only. Output romaji without explanation."},
{"role": "user", "content": text}
]
return await chat_with_openai(messages)
async def process_lrc_file(filepath: str) -> FileResult:
added_lines = 0
with open(filepath, "r", encoding="utf-8") as f:
lines = f.readlines()
new_lines = []
for idx, line in enumerate(lines):
new_lines.append(line)
if timestamp_pattern.match(line) and needs_romaji(lines, idx):
japanese = line.strip().split("]", 1)[-1].strip()
if japanese:
romaji = await get_romaji(japanese)
new_lines.append(f"{romaji}\n")
added_lines += 1
if added_lines > 0:
with open(filepath, "w", encoding="utf-8") as f:
f.writelines(new_lines)
return FileResult(file=filepath, processed=added_lines > 0, added_lines=added_lines)
async def safe_process(filepath):
async with semaphore:
print(f"Processing: {filepath}")
return await process_lrc_file(filepath)
async def translate_lyric_romanji(folder_path: str):
results = []
if not os.path.exists(folder_path):
return {"results": [], "status": f"error: folder not found {folder_path}"}
tasks = []
for root, _, files in os.walk(folder_path):
for file in files:
if file.endswith(".lrc"):
filepath = os.path.join(root, file)
tasks.append(asyncio.create_task(safe_process(filepath)))
if not tasks:
return {"results": [], "status": "no .lrc files found"}
# Run them all concurrently
results = await asyncio.gather(*tasks)
return {"results": results, "status": "completed"}

View File

@@ -0,0 +1,88 @@
import os
import asyncio
from app.core.deepseek_client import chat_with_openai
from app.models.text_generation import BPOMMobileResponseTextGenerationRequest, BPOMMobileResponseTextGenerationResponse
async def generate_text(request: BPOMMobileResponseTextGenerationRequest) -> BPOMMobileResponseTextGenerationResponse:
messages = [
{"role": "system", "content": """
Anda adalah asisten virtual resmi BPOM (Badan Pengawas Obat dan Makanan) yang bertugas menanggapi keluhan dan review pengguna aplikasi BPOM Mobile.
Tugas Utama:
Merespons semua jenis review (negatif, positif, netral) dengan profesional dan empati
Fokus pada solusi dan bantuan teknis
Menjaga citra positif institusi BPOM
Panduan Respons:
Untuk keluhan teknis (scan error, akses lambat, dll):
Awali dengan permintaan maaf yang tulus
Sarankan update aplikasi ke versi terbaru
Informasikan perbaikan berkelanjutan
Sediakan kontak support: barcodebpom@pom.go.id
Untuk review positif:
Ucapkan terima kasih
Tegaskan komitmen untuk terus meningkatkan kualitas
Dorong untuk terus menggunakan aplikasi
Untuk review negatif dengan emosi tinggi:
Tunjukkan empati lebih dalam
Hindari jargon teknis
Berikan solusi alternatif (input manual nomor registrasi)
Tawarkan jalur eskalsi via email
Format Respons:
Gunakan sapaan "Sobat Cerdas BPOM"
Bahasa informal namun profesional
Maksimal 3-4 kalimat
Selalu sertakan opsi kontak support
Contoh Respons yang Diinginkan:
Untuk review negatif:
"Sobat Cerdas BPOM, mohon maaf atas kendala yang dialami. Tim kami terus melakukan perbaikan sistem. Untuk alternatif sementara, Sobat dapat memasukkan nomor registrasi secara manual. Jika kendala berlanjut, silakan hubungi barcodebpom@pom.go.id untuk bantuan lebih lanjut."
Untuk review positif:
"Terima kasih atas apresiasi dan masukannya, Sobat Cerdas BPOM! Semangat ini akan kami teruskan untuk memberikan pelayanan terbaik. Jangan ragu untuk memberikan saran pengembangan lainnya ya!"
Penyesuaian Dinamis:
Sesuaikan tingkat empati berdasarkan tingkat emosi review
Untuk review dengan emosi sangat tinggi, tambahkan kalimat penenang
Untuk masalah spesifik, berikan solusi yang lebih terarah
Batasan:
Tidak membuat janji perbaikan yang tidak dapat ditepati
Tidak menyalahkan pengguna
Tidak memberikan respons template yang sama persis
Menghindari istilah teknis yang rumit
"""},
{"role": "user", "content": request.text}
]
generated_text = await chat_with_openai(messages)
return BPOMMobileResponseTextGenerationResponse(generated_text=generated_text)

10
app/services/voice.py Normal file
View File

@@ -0,0 +1,10 @@
from app.core.openai_voice_transcription_client import generate_voice, generate_transcription
async def generate_voice(text: str) -> str:
voice = await generate_voice(text)
return voice
async def generate_transcription(audio_file_path: str) -> str:
transcription = await generate_transcription(audio_file_path)
return transcription

31
readme.md Normal file
View File

@@ -0,0 +1,31 @@
# Project Title
A brief description of what this project does and who it's for
## Run Locally
Clone the project
```bash
git clone https://link-to-project
```
Go to the project directory
```bash
cd my-project
```
Install dependencies
```bash
pip install -r requirements.txt
```
Start the server
```bash
uvicorn app.main:app --reload
```