sushilideaclan01 commited on
Commit
8cab861
·
1 Parent(s): 8ffe335
Files changed (2) hide show
  1. frontend/lib/api/endpoints.ts +33 -4
  2. main.py +111 -47
frontend/lib/api/endpoints.ts CHANGED
@@ -93,7 +93,20 @@ export const generateMotivators = async (
93
  return response.data;
94
  };
95
 
96
- // Extensive Endpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  export const generateExtensiveAd = async (params: {
98
  niche: Niche;
99
  custom_niche?: string | null;
@@ -103,7 +116,6 @@ export const generateExtensiveAd = async (params: {
103
  image_model?: string | null;
104
  num_strategies: number;
105
  }): Promise<BatchResponse> => {
106
- // Ensure required parameters are always sent
107
  const requestParams = {
108
  niche: params.niche,
109
  num_images: params.num_images || 1,
@@ -113,8 +125,25 @@ export const generateExtensiveAd = async (params: {
113
  ...(params.offer && { offer: params.offer }),
114
  ...(params.image_model && { image_model: params.image_model }),
115
  };
116
- const response = await apiClient.post<BatchResponse>("/extensive/generate", requestParams);
117
- return response.data;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  };
119
 
120
  export const generateTestingMatrix = async (params: {
 
93
  return response.data;
94
  };
95
 
96
+ // Extensive Endpoints (async job pattern to avoid connection timeout on HF Spaces)
97
+ const EXTENSIVE_POLL_INTERVAL_MS = 5500;
98
+ const EXTENSIVE_POLL_TIMEOUT_MS = 20 * 60 * 1000; // 20 minutes
99
+
100
+ export const getExtensiveJobStatus = async (jobId: string): Promise<{ job_id: string; status: "running" | "completed" | "failed"; error?: string }> => {
101
+ const response = await apiClient.get(`/extensive/status/${jobId}`);
102
+ return response.data;
103
+ };
104
+
105
+ export const getExtensiveJobResult = async (jobId: string): Promise<BatchResponse> => {
106
+ const response = await apiClient.get<BatchResponse>(`/extensive/result/${jobId}`);
107
+ return response.data;
108
+ };
109
+
110
  export const generateExtensiveAd = async (params: {
111
  niche: Niche;
112
  custom_niche?: string | null;
 
116
  image_model?: string | null;
117
  num_strategies: number;
118
  }): Promise<BatchResponse> => {
 
119
  const requestParams = {
120
  niche: params.niche,
121
  num_images: params.num_images || 1,
 
125
  ...(params.offer && { offer: params.offer }),
126
  ...(params.image_model && { image_model: params.image_model }),
127
  };
128
+ const response = await apiClient.post<{ job_id: string; message?: string }>("/extensive/generate", requestParams);
129
+ const jobId = response.data?.job_id;
130
+ if (!jobId) {
131
+ throw new Error("Server did not return a job ID");
132
+ }
133
+ const deadline = Date.now() + EXTENSIVE_POLL_TIMEOUT_MS;
134
+ for (;;) {
135
+ const status = await getExtensiveJobStatus(jobId);
136
+ if (status.status === "completed") {
137
+ return getExtensiveJobResult(jobId);
138
+ }
139
+ if (status.status === "failed") {
140
+ throw new Error(status.error || "Extensive generation failed");
141
+ }
142
+ if (Date.now() >= deadline) {
143
+ throw new Error("Extensive generation timed out. Try fewer strategies or images.");
144
+ }
145
+ await new Promise((r) => setTimeout(r, EXTENSIVE_POLL_INTERVAL_MS));
146
+ }
147
  };
148
 
149
  export const generateTestingMatrix = async (params: {
main.py CHANGED
@@ -12,6 +12,7 @@ from fastapi.responses import FileResponse, StreamingResponse, Response as FastA
12
  from pydantic import BaseModel, Field
13
  from typing import Optional, List, Literal, Any, Dict
14
  from datetime import datetime
 
15
  import os
16
  import logging
17
  import time
@@ -1810,9 +1811,12 @@ async def motivator_generate_endpoint(
1810
 
1811
 
1812
  # =============================================================================
1813
- # EXTENSIVE ENDPOINTS
1814
  # =============================================================================
1815
 
 
 
 
1816
  class ExtensiveGenerateRequest(BaseModel):
1817
  """Request for extensive generation."""
1818
  niche: str = Field(
@@ -1848,58 +1852,118 @@ class ExtensiveGenerateRequest(BaseModel):
1848
  )
1849
 
1850
 
1851
- @app.post("/extensive/generate", response_model=BatchResponse)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1852
  async def generate_extensive(
1853
  request: ExtensiveGenerateRequest,
1854
  username: str = Depends(get_current_user)
1855
  ):
1856
  """
1857
- Generate ad using extensive: researcher → creative director → designer → copywriter.
1858
-
1859
- Requires authentication. Users can only see their own generated ads.
1860
-
1861
- This flow:
1862
- 1. Researches psychology triggers, angles, and concepts
1863
- 2. Retrieves marketing book knowledge and old ads data
1864
- 3. Creates creative strategies
1865
- 4. Generates image prompts and ad copy in parallel
1866
- 5. Generates images for each strategy
1867
-
1868
- Returns all generated ads from all strategies (like batch generation).
1869
-
1870
- Supports custom niches via the 'others' option - when niche is 'others',
1871
- custom_niche field must be provided with the custom niche name.
1872
  """
1873
- try:
1874
- # Determine effective niche
1875
- if request.niche == "others":
1876
- if not request.custom_niche or not request.custom_niche.strip():
1877
- raise HTTPException(
1878
- status_code=400,
1879
- detail="custom_niche is required when niche is 'others'"
1880
- )
1881
- effective_niche = request.custom_niche.strip()
1882
- else:
1883
- effective_niche = request.niche
1884
-
1885
- results = await ad_generator.generate_ad_extensive(
1886
- niche=effective_niche,
1887
- target_audience=request.target_audience,
1888
- offer=request.offer,
1889
- num_images=request.num_images,
1890
- image_model=request.image_model,
1891
- num_strategies=request.num_strategies,
1892
- username=username, # Pass current user
1893
- )
1894
- # Return as BatchResponse format
1895
- return BatchResponse(
1896
- count=len(results),
1897
- ads=results
 
 
 
1898
  )
1899
- except HTTPException:
1900
- raise
1901
- except Exception as e:
1902
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1903
 
1904
 
1905
  # =============================================================================
@@ -2595,7 +2659,7 @@ async def frontend_proxy(path: str, request: StarletteRequest):
2595
  # Note: /health has its own explicit route, so not listed here
2596
  api_only_routes = [
2597
  "auth/login", "api/correct", "api/download-image", "api/export/bulk",
2598
- "db/stats", "db/ads", "strategies", "extensive/generate"
2599
  ]
2600
 
2601
  # Routes that are API for POST but frontend for GET
 
12
  from pydantic import BaseModel, Field
13
  from typing import Optional, List, Literal, Any, Dict
14
  from datetime import datetime
15
+ import asyncio
16
  import os
17
  import logging
18
  import time
 
1811
 
1812
 
1813
  # =============================================================================
1814
+ # EXTENSIVE ENDPOINTS (async job pattern to avoid connection timeout on HF Spaces)
1815
  # =============================================================================
1816
 
1817
+ # In-memory job store: job_id -> { status, result?, error?, username }
1818
+ _extensive_jobs: Dict[str, Dict[str, Any]] = {}
1819
+
1820
  class ExtensiveGenerateRequest(BaseModel):
1821
  """Request for extensive generation."""
1822
  niche: str = Field(
 
1852
  )
1853
 
1854
 
1855
+ class ExtensiveJobResponse(BaseModel):
1856
+ """Response when extensive generation is started (202 Accepted)."""
1857
+ job_id: str
1858
+ message: str = "Extensive generation started. Poll /extensive/status/{job_id} for progress."
1859
+
1860
+
1861
+ async def _run_extensive_job_async(
1862
+ job_id: str,
1863
+ username: str,
1864
+ effective_niche: str,
1865
+ target_audience: Optional[str],
1866
+ offer: Optional[str],
1867
+ num_images: int,
1868
+ image_model: Optional[str],
1869
+ num_strategies: int,
1870
+ ):
1871
+ """Run extensive generation on the main event loop so DB and other async code use the same loop."""
1872
+ try:
1873
+ results = await ad_generator.generate_ad_extensive(
1874
+ niche=effective_niche,
1875
+ target_audience=target_audience,
1876
+ offer=offer,
1877
+ num_images=num_images,
1878
+ image_model=image_model,
1879
+ num_strategies=num_strategies,
1880
+ username=username,
1881
+ )
1882
+ _extensive_jobs[job_id]["status"] = "completed"
1883
+ _extensive_jobs[job_id]["result"] = BatchResponse(count=len(results), ads=results)
1884
+ except Exception as e:
1885
+ api_logger.exception("Extensive job %s failed", job_id)
1886
+ _extensive_jobs[job_id]["status"] = "failed"
1887
+ _extensive_jobs[job_id]["error"] = str(e)
1888
+
1889
+
1890
+ @app.post("/extensive/generate", status_code=202)
1891
  async def generate_extensive(
1892
  request: ExtensiveGenerateRequest,
1893
  username: str = Depends(get_current_user)
1894
  ):
1895
  """
1896
+ Start extensive ad generation (researcher → creative director → designer → copywriter).
1897
+ Returns 202 with job_id. Poll GET /extensive/status/{job_id} then GET /extensive/result/{job_id}.
1898
+ Runs on the main event loop so DB (MongoDB) and other async code stay on the same loop.
 
 
 
 
 
 
 
 
 
 
 
 
1899
  """
1900
+ if request.niche == "others":
1901
+ if not request.custom_niche or not request.custom_niche.strip():
1902
+ raise HTTPException(
1903
+ status_code=400,
1904
+ detail="custom_niche is required when niche is 'others'"
1905
+ )
1906
+ effective_niche = request.custom_niche.strip()
1907
+ else:
1908
+ effective_niche = request.niche
1909
+
1910
+ job_id = str(uuid.uuid4())
1911
+ _extensive_jobs[job_id] = {
1912
+ "status": "running",
1913
+ "result": None,
1914
+ "error": None,
1915
+ "username": username,
1916
+ }
1917
+
1918
+ asyncio.create_task(
1919
+ _run_extensive_job_async(
1920
+ job_id,
1921
+ username,
1922
+ effective_niche,
1923
+ request.target_audience,
1924
+ request.offer,
1925
+ request.num_images,
1926
+ request.image_model,
1927
+ request.num_strategies,
1928
  )
1929
+ )
1930
+ return ExtensiveJobResponse(job_id=job_id)
1931
+
1932
+
1933
+ @app.get("/extensive/status/{job_id}")
1934
+ async def extensive_job_status(
1935
+ job_id: str,
1936
+ username: str = Depends(get_current_user)
1937
+ ):
1938
+ """Get status of an extensive generation job."""
1939
+ if job_id not in _extensive_jobs:
1940
+ raise HTTPException(status_code=404, detail="Job not found")
1941
+ job = _extensive_jobs[job_id]
1942
+ if job["username"] != username:
1943
+ raise HTTPException(status_code=404, detail="Job not found")
1944
+ return {
1945
+ "job_id": job_id,
1946
+ "status": job["status"],
1947
+ "error": job.get("error") if job["status"] == "failed" else None,
1948
+ }
1949
+
1950
+
1951
+ @app.get("/extensive/result/{job_id}", response_model=BatchResponse)
1952
+ async def extensive_job_result(
1953
+ job_id: str,
1954
+ username: str = Depends(get_current_user)
1955
+ ):
1956
+ """Get result of a completed extensive generation job. Returns 404 if not found, 425 if still running."""
1957
+ if job_id not in _extensive_jobs:
1958
+ raise HTTPException(status_code=404, detail="Job not found")
1959
+ job = _extensive_jobs[job_id]
1960
+ if job["username"] != username:
1961
+ raise HTTPException(status_code=404, detail="Job not found")
1962
+ if job["status"] == "running":
1963
+ raise HTTPException(status_code=425, detail="Generation still in progress")
1964
+ if job["status"] == "failed":
1965
+ raise HTTPException(status_code=500, detail=job.get("error", "Generation failed"))
1966
+ return job["result"]
1967
 
1968
 
1969
  # =============================================================================
 
2659
  # Note: /health has its own explicit route, so not listed here
2660
  api_only_routes = [
2661
  "auth/login", "api/correct", "api/download-image", "api/export/bulk",
2662
+ "db/stats", "db/ads", "strategies", "extensive/generate", "extensive/status", "extensive/result"
2663
  ]
2664
 
2665
  # Routes that are API for POST but frontend for GET